Programming
디자인 패턴
foxlee
2022. 12. 23. 17:47
* 헤드퍼스트 디자인패턴(개정판), Node.js 디자인 패턴 바이블 책을 보며 이해하고 작성, 참고 또는 직접 작성한 코드를 적어본다.
* 구상(Concrete) 클래스: new 키워드를 사용하여 인스턴스를 만드는 클래스(=구현 클래스)
* 추상(Abstract) 클래스: 하나의 추상 메소드를 포함하는 클래스로 이를 상속하는 자식 클래스는 추상 메소드를 구현해야함
* 인터페이스(Interface): 클래스의 공통된 기능을 정리한 명세서
* 컨택스트(Context): 사용자가 관심 있는 인터페이스를 정의/문제에 대한 정의,상황
옵저버
전략 패턴
- 특정 변경이 필요한 부분을 별도의 상호 교환 가능한 (전략) 객체로 추출하여 로직의 변경이 용이하도록 함
- 추출하여 합성하는 것이 포인트
- 해당 패턴을 알기전에 합성하는 부분에 대해서는 경험이 있었으나 요구상황에 따라 다르게 합성하는 것에 대한 경험은 없었고 사실 fly/flyable과 같이 나눈다는 것에 대한 개념은 그전엔 생각해보지 않았던 것 같음
from abc import *
class AbstractAnimal(metaclass=ABCMeta):
name = "name"
weight = 0
height = 0
@abstractmethod
def description(self):
print('description')
class Flyable():
def fly(self):
print("fly")
class NonFlyable():
def fly(self):
print("can't fly")
class Runable():
def run(self):
print("run")
class NonRunable():
def run(self):
print("can't run")
class Dog(AbstractAnimal, Runable, NonFlyable):
def __init__(self, name, weight, height):
self.name = name
self.weight = weight
self.height = height
def description(self):
print(f"dog description, name: {self.name}, weight: {self.weight}, height: {self.height}")
Albi = Dog("Albi", 22, 100)
Albi.description()
Albi.run()
Albi.fly()
class Bird(AbstractAnimal, NonRunable, Flyable):
def __init__(self, name, weight, height):
self.name = name
self.weight = weight
self.height = height
def description(self):
print(f"bird description, name: {self.name}, weight: {self.weight}, height: {self.height}")
Sevi = Bird("Sevi", 16, 60)
Sevi.description()
Sevi.run()
Sevi.fly()
상태 패턴
- 전략 패턴과 유사할수 있으나 대상 컴포넌트(=컨택스트)의 상태를 추출한 클래스에서 각 상태에 대응되는 작업을 수행함
- 컨택스트 메서드 내의 상태에 따른 조건문 코드를 없애고 상태 클래스로 모든 작업을 위임함
- 내부 상태가 변경할때 행동이 달라지게 되고 컨택스트는 그대로이나 바뀐 것 같은 결과를 얻음
from __future__ import annotations
from abc import ABC, abstractmethod
from time import time
class FailsafeSocket:
def __init__(self) -> None:
self.queue = []
self.states = {
"offline": Offline(self),
"online": Online(self),
"pending": Pending(self),
}
self.change_state("offline")
def change_state(self, state):
self.state = self.states[state]
def send(self, data):
self.state.send(data)
class AbstractState(ABC):
@abstractmethod
def send(self) -> None:
pass
class Offline(AbstractState):
def __init__(self, FailsafeSocket):
self.socket = FailsafeSocket
def send(self, data) -> None:
print(f"{type(self).__name__} {data} not sent ")
class Pending(AbstractState):
def __init__(self, FailsafeSocket):
self.socket = FailsafeSocket
def send(self, data) -> None:
self.socket.queue.append(data)
print(f"{type(self).__name__} data {data} added to queue: {self.socket.queue}")
class Online(AbstractState):
def __init__(self, FailsafeSocket):
self.socket = FailsafeSocket
def send(self, data) -> None:
if len(data) > 5:
self.socket.change_state("offline")
print(f"{type(self).__name__} too long data {data}, change to noline")
self.socket.queue = []
else:
self.socket.queue.append(data)
print(f"{type(self).__name__} sent data: {self.socket.queue}")
self.socket.queue = []
# 상태가 ON/OFF 일때 context의 send에서 분기가 있는 것이 아니라 해당 상태 객체에 정의된 메서드에 의해 할일을 수행
# 상태가 여러개라고 가저한다면 if 선언문을 제거할수 있고
# 상태의 변경에는 닫혀있지만 상태 확장에는 열려있음
if __name__ == "__main__":
socket = FailsafeSocket()
socket.send("1")
socket.send("2")
socket.send("3")
socket.change_state("pending")
socket.send("4")
socket.send("5")
socket.send("6")
socket.change_state("online")
socket.send("7")
socket.send("8")
socket.send("9")
socket.send("11111111")
socket.send("1")
# socket1 = FailsafeSocket()
# socket1.send("1")
# socket1.send("2")
# socket1.send("3")
# socket1.change_state("pending")
# socket1.send("4")
# socket1.send("5")
# socket1.send("6")
# socket1.change_state("online")
# socket1.send("7")
# socket1.send("8")
# socket1.send("9")
# print(socket1 == socket) # false
# print(socket.state.socket == socket1.state.socket) # false
- 전략과 상태 패턴
- 전략 패턴은 어떤 전략 객체를 사용할지 클라이언트가 지정해주지만(=>처음 실행 시 객체 변경의 유연성)
- 상태 패턴은 행동을 상태 객체 내에 캡슐화하여 Context 내의 상태 객체를 바꾸는 것만으로 객체의 행동을 바꾼다.
템플릿 메서드 패턴
- 부모 클래스에서 추상 메서드를 포함한 일부 메서드들을 구현하고, 자식 클래스에서 요구사항에 따라 다른 메서드들(추상 메서드, 메서드 오버라이드(옵션))을 구현하여 특정 단계들만 확장하기 위함
- 상속을 기반을 함
- 클래스를 만들때 템플릿 메서드 패턴인지 모르고 이 패턴을 사용한 경험이 많이 있음
- 전략과 템플릿 메서드패턴
- 전략은 구상클래스들을 합성, 템플릿 메서드는 자식 클래스들을 확장하는 것에 초점
- 전략 패턴은은 객체 수준에서 작동하므로 런타임에 행동들을 전환, 템플릿 메서드는 클래스 수준에서 작동하므로 정적
싱글턴 패턴
- Python의 Flask 에서 app 이라는 객체를 바로 실행하는 것이 아니라 create_app()이라는 함수를 호출하여 app을 받도록 하는 부분에서 많이 사용했었고, DB Connection에 적용했던 경험이 있음
팩토리 패턴
빌더 패턴
- 객체 생성을 단순화하는 생성 디자인 패턴 -> 단계별 객체 생성하거나 복잡한 객체를 만들때 가독성 및 사용성이 향상됨
- 장점을 최대화할수 있는 경우는 인자가 목록이 길거나 복잡한 매개변수 입력을 사용하는 생성자가 있는 클래스
- 예를 들어 입력되던 파라미터가 변경이 필요할때 손쉽게 해결할 수 있음
- 파라미터 입력값이 많아질수록 복잡하고 순서를 외우야함
- 파이썬의 경우 키워드 파라미터가 존재해서 빌더 패턴의 장점이 큰 장점이 아니다. 바로 키워드를 지정해서 진행할 수 있고 이경우 순서또한 상관 없음
class UrlBuilder {
setProtocol(protocol) {
this.protocol = protocol
return this
}
setHost(host) {
this.host = host
return this
}
setPort(port) {
this.port = port
return this
}
build() {
return `${this.protocol}://${this.host}:${this.port}`
}
}
const url = new UrlBuilder()
.setProtocol('http')
.setHost('localhost')
.setPort('3000')
.build()
console.log(url) // http://localhost:3000
커맨드 패턴
데코레이터 패턴
어댑터 패턴
- 특정 클래스 인터페이스를 클라이언트에서 요구하는 다른 인터페이스로 변환: 인터페이스가 호환되지 않아 사용할 수 없는 클래스를 사용할 수 있게 해준다.
퍼사드패턴
- 인터페이스를 단순하게 만들고 클라이언트와 구성 요소로 이루어진 서브시스템으로 분리함
- NestJS에서 Controller에서 여러 서비스 레이어들을 의존할때 이 수가 많아진다면 이를 분리하여 하나의 퍼사드레이어를 의존하고 퍼사드 레이어에서 기존 서비스들을 의존하게 하여 분리/단순하게 함
export class AggregationFacade {
constructor(
private readonly _userService: UserService,
private readonly _orderService: OrderService,
private readonly _paymentService: PaymentService,
) {}
}
어댑터 패턴과 퍼사드 패턴
- 둘다 여러 개의 클래스를 감쌀 수 있지만, 퍼사드는 인터페이스를 단순하게 만드는 용도이고 어댑터는 인터페이스를 다른 인터페이스를 변환함
....