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,
  ) {}
}

어댑터 패턴과 퍼사드 패턴

  • 둘다 여러 개의 클래스를 감쌀 수 있지만, 퍼사드는 인터페이스를 단순하게 만드는 용도이고 어댑터는 인터페이스를 다른 인터페이스를 변환함

 

 

....