디자인 패턴#

디자인 패턴의 이해#

새 프로젝트를 시작할 때 가장 먼저 하는 일이 과거에 수행한 유사 프로젝트를 찾아 검토하는 일이다. 이를 통해 개발 기간, 비용, 필요 인력, 개발 조직 등을 참고할 수 있고, 개발 방법도 가져다 적용할 수 있기 때문이다. 경험을 잘 활용하면 일을 매우 효율적으로 할 수 있다. 소프트웨어를 설계할 때도 경험만큼 확실한 것은 없다. 경험이 적은 사람도 설계를 잘 하려면 경험 많은 전문가의 지식과 노하우를 공유하는 것이 필요하다. 소프트웨어 설계에 대한 지식과 노하우는 어떻게 공유할 수 있을까? 먼저, 소프트웨어 설계 방법론과 지침을 공유할 수 있다. 그러나 한계가 있다. 방법론이나 지침은 일반화된 것이어서 구체적인 문제에 적용하려면 또 다른 지식이나 노하우가 필요하기 때문이다. 객체지향 방법론을 아는 것과 내가 개발할 소프트웨어에 적용하는 것은 차이가 있다. 여러 가지 설계 사례를 공유하는 건 어떨까. 그러나 이 방법도 사례와 전혀 다른 유형의 문제를 해결하려면 또 다른 노력이 필요하다. 개별 사례는 너무 구체적이고 특정 문제의 가정에 의존적이기 때문이다. 예를 들어 일괄 처리를 목적으로 하는 소프트웨어를 설계할 때의 지식이나 노하우를 실시간 처리를 목적으로 하는 소프트웨어 설계에 적용하기는 힘들다. 그렇다면 너무 일반적이지도 너무 구체적이지도 않은 형태로 소프트웨어 설계를 위한 지식이나 노하우를 공유할 방법이 필요하다. 이를 위해 고안된 것이 바로 GoF(Gang of Four)의 디자인 패턴으로 객체지향 개념에 따른 설계 중에서 재사용할 경우에 유용한 것을 디자인 패턴으로 정립한 것이다.

디자인 패턴은 여러 가지 설계 사례를 분석해 비슷한 문제를 해결하기 위한 설계로 분류하고 유형별로 가장 적합한 설계를 일반화해 패턴으로 정립한 것이다. 디자인 패턴은 소프트웨어 설계에 대한 지식이나 노하우가 문제 유형별로 잘 구체화되어 있고 동일한 유형의 문제를 해결하는 방법에 대한 지식이나 노하우가 패턴 형태로 충분히 일반화된 것이라 할 수 있다. 단, 쉽게 재사용할 수 있도록 객체지향 개념에 따른 설계만을 패턴으로 지정했다. 사실 소프트웨어는 목적이 다 다르고 구체적인 적용 상황이나 환경에 따라 최적의 해결책이 달라지는 만큼 일정한 패턴을 여러 소프트웨어 설계에 적용한다는 것이 이해가 되지 않을 수 있다. 그러나 주어진 상황이나 문제를 세분화해서 분류해보면 많은 문제가 전에도 경험했던 것임을 알 수 있다. 예를 들어 객체를 생성하거나 공유하는 문제, 모듈 간의 연관 관계를 간단하게 만들거나 특정 모듈에서 수행해야 할 역활을 위임하는 문제 등은 소프트웨어를 설계할 때 자주 접하는 것이다. 따라서 이미 여러 사람에 의해 검증된 디자인 패턴을 활용한다면 경험이 부족하더라도 충분히 좋은 소프트웨어를 설계할 수 있을 것이다. GoF의 디자인 패턴은 목적에 따라 생성 패턴, 구조 패턴, 행위 패턴으로 분류할 수 있고 총 23개의 구체적인 패턴이 있다. 생성 패턴은 객체를 생성하는데 관련된 패턴으로 객체가 생성되는 과정의 유연성을 높이고 코드의 유지를 쉽게 한다. 구조 패턴은 프로그램 구조에 관련된 패턴으로 프로그램 내 자료구조나 인터페이스 구조 등 프로그램의 구조를 설계하는데 활용할 수 있는 패턴이다. 행위 패턴은 반복적으로 사용되는 객체들의 상호작용을 패턴화 해놓은 것들이다.

GoF라 불리는 사람들은 에리히 감마(Erich Gamma), 리차드 헬름(Richard Helm), 랄프 존슨(Ralph Johnson), 존 블리시디스(John Vissides)이다. 이들은 소프트웨어 개발 영역에서 디자인 패턴을 구체화하고 체계화한 사람들로 23가지의 디자인 패턴을 정리하고 각 디자인 패턴을 생성, 구조, 행위 세 가지로 분류하였다.

생성 패턴#

프로그램은 규모가 커질수록 복잡해지고 객체의 생성과 참조 횟수가 많아질 수밖에 없다. 이는 유지관리 및 재사용을 어렵게 한다. 생성 패턴(추상 객체 인스턴스화, creational pattern)은 은 객체의 생성과 참조 과정을 추상화해 특정 객체의 생성 과정을 분리함으로써 누가 언제 변경하더라도 전체 시스템에 미치는 영향을 최소화하도록 만들어준다. 이러한 디자인 패턴은 모두 클래스 인스턴스화에 관한 것이다. 이 패턴은 클래스 생성 패턴과 객체 생성 패턴으로 더 나눌 수 있다. 클래스 생성 패턴은 인스턴스화 프로세스에서 상속을 효과적으로 사용하지만 객체 생성 패턴은 위임을 효과적으로 사용하여 작업을 완료한다. 따라서 코드의 유연성을 높일 수 있고 유지관리를 쉽게 만들어 준다. 생성 패턴의 종류는 다음과 같다.

  • 추상 팩토리(abstract factory) 패턴: 여러 클래스 패밀리의 개체를 만듬.

  • 팩토리(factory method) 패턴: 여러 파생 클래스의 인스턴스를 만듬.

  • 빌더(builder) 패턴: 객체 생성을 자신의 표현과 분리함.

  • 프로토타입(prototype) 패턴: 복사 또는 복제할 완전히 초기화된 인스턴스

  • 싱글톤(singleton) 패턴: 하나의 인스턴스만 존재할 수 있는 클래스

추상 팩토리#

의도#

추상 팩토리(abstract factory)는 구체적인 클래스를 지정하지 않고 관련성을 갖는 객체들의 집합을 생성하거나 서로 독립적인 객체들의 집합을 생성할 수 있는 인터페이스를 제공하는 패턴이다. 캡슐화하는 계층은 가능한 많은 “플랫폼” 및 “제품”의 제품군의 구성하고, 새로운 명령어는 유해한 것으로 간주한다. 애플리케이션이 이식 가능하려면 플랫폼 종속성을 캡슐화해야 한다. 이러한 “플랫폼”에는 윈도우 시스템, 운영 체제, 데이터베이스 등이 포함될 수 있다. 이 캡슐화는 사전에 설계되지 않으며 현재 지원되는 모든 플랫폼에 대한 옵션이 포함된 많은 #ifdef case 문은 코드 전체에서 토끼처럼 번식하기 시작한다.

활용성#

  • 객체가 생성되거나 구성 및 표현되는 방식과 무관하게 시스템을 독집적으로 만들고 자 할 때

  • 여러 제품군 중 하나를 선택해서 시스템을 설정해야 하고 한번 구성한 제품을 다른 것으로 대체할 수 있을 때

  • 관련된 제품 객체들이 함께 사용되도록 설계되었고, 이 부분에 대한 제약이 외부에도 지켜지도록 하고 싶을 때

  • 제품에 대한 클래스 라이브러리를 제공하고, 그들의 구현이 아닌 인터페이스를 노출하고 싶을 때

구조#

추상 팩토리는 제품별로 팩토리 메서드(factory method)를 정의한다. 각 팩토리 메서드는 새로운 연산자와 구체적인 플랫폼별 제품 클래스를 캡슐화한다. 그런 다음 각 “플랫폼”은 팩토리 파생 클래스로 모델링된다.

추상 팩토리의 목적은 구체적인 클래스를 지정하지 않고 관련 객체의 패밀리를 생성하기 위한 인터페이스를 제공하는 것이다. 이 패턴은 자동차 제조에 사용되는 판금 스탬핑(stamping) 장비에서 볼 수 있다. 스탬핑 장비는 자동차 차체 부품을 만드는 추상 공장이다. 다른 모델의 자동차에 대해 오른쪽 문, 왼쪽 문, 오른쪽 앞 흙받이, 왼쪽 앞 흙받이, 후드 등을 스탬핑하는 데 동일한 기계가 사용된다. 롤러(roller)를 사용하여 스탬핑 다이를 교체함으로써 기계에서 생산된 콘크리트 등급을 3분 이내에 변경할 수 있다.

결과#

  • 구체적인 클래스를 분리한다.

  • 제품군을 쉽게 대체할 수 있도로 한다.

  • 제품 사이의 일관성을 증진시킨다.

  • 새로운 종류의 제품을 제공하기 어렵다.

from abc import ABC, abstractmethod


class AbstractFactory(ABC):
    """
    추상 제품 객체를 생성하는 작업에 대한 인터페이스를 선언함.
    """
    @abstractmethod
    def create_product_a(self):
        pass

    @abstractmethod
    def create_product_b(self):
        pass


class ConcreteFactory1(AbstractFactory):
    """
    구상(concrete) 제품 객체를 만드는 작업을 구현함.
    """
    def create_product_a(self):
        return ConcreteProductA1()

    def create_product_b(self):
        return ConcreteProductB1()


class ConcreteFactory2(AbstractFactory):
    """
    구상(concrete) 제품 객체를 만드는 작업을 구현함.
    """
    def create_product_a(self):
        return ConcreteProductA2()

    def create_product_b(self):
        return ConcreteProductB2()


class AbstractProductA(ABC):
    """
    제품 객체 유형에 대한 인터페이스를 선언함.
    """
    @abstractmethod
    def interface_a(self):
        pass


class ConcreteProductA1(AbstractProductA):
    """
    해당 구상 팩토리에서 생성할 제품 객체를 정의함. 
    AbstractProduct 인터페이스를 구현함.
    """
    def interface_a(self):
        pass


class ConcreteProductA2(AbstractProductA):
    """
    해당 구상 팩토리에서 생성할 제품 객체를 정의함. 
    AbstractProduct 인터페이스를 구현함.
    """
    def interface_a(self):
        pass


class AbstractProductB(ABC):
    """
    제품 객체 유형에 대한 인터페이스를 선언함.
    """
    @abstractmethod
    def interface_b(self):
        pass


class ConcreteProductB1(AbstractProductB):
    """
    해당 구상 팩토리에서 생성할 제품 객체를 정의함. 
    AbstractProduct 인터페이스를 구현함.
    """
    def interface_b(self):
        pass


class ConcreteProductB2(AbstractProductB):
    """
    해당 구상 팩토리에서 생성할 제품 객체를 정의함. 
    AbstractProduct 인터페이스를 구현함.
    """
    def interface_b(self):
        pass


def main():
    for factory in (ConcreteFactory1(), ConcreteFactory2()):
        product_a = factory.create_product_a()
        product_b = factory.create_product_b()
        product_a.interface_a()
        product_b.interface_b()

팩토리 메서드#

의도#

팩토리 메서드(factory method)는 객체를 생성하는 인터페이스를 미리 정의하되, 인스턴스를 만들 클래스의 결정은 서브클래스에서 내리는 패턴이다. 팩토리 메서드 패턴은 클래스의 인스턴스를 만드는 시점을 서브클래스로 미룬다. 자세히 말하면, 개체 생성을 하위 클래스에게 위임하는 것으로 개체를 만드는 방법은 상위 클래스에서 결정하지만 구체적인 클래스명을 결정하지 않는다. 이렇게하면 개체 생성을 위한 프레임워크와 실제 개체를 생성하는 클래스를 분리하면 결합도가 낮아진다.

활용성#

  • 어떤 클래스가 자신이 생성해야 하는 객체의 클래스를 예측할 수 없을 때

  • 생성할 객체를 기술하는 책임을 자신의 서브클래스가 지정했으면 할 때

  • 객체 생성의 책임을 몇 개의 보조 서브클래스 가운데 하나에게 위임하고, 어떤 서브클래스가 위임자인지에 대한 정보를 국소화시키고 싶을 때

구조#

GoF에서 논의된 팩토리 메서드의 구현은 추상 팩토리의 구현과 겹친다.

팩토리 메서드에 대한 정의는 다음과 같다. 해당 클래스 유형의 개체를 반환하는 클래스의 정적 메서드이다. 그러나 생성자와 달리 반환하는 실제 개체는 하위 클래스의 인스턴스일 수 있다. 생성자와 달리 새 개체를 만드는 대신 기존 개체를 재사용할 수 있다. 생성자와 달리 팩토리 메서드는 다르고 더 설명적인 이름을 가질 수 있다(예: Color.make_RGB_color(float red, float green, float blue) 및 Color.make_HSB_color(float hue, float saturation, float brightness).

클라이언트는 파생 클래스의 구현 세부 정보에서 완전히 분리되며 다형성 생성이 가능하다.

팩토리 메서드는 객체 생성을 위한 인터페이스를 정의하지만, 서브 클래스가 인스턴스화할 클래스를 결정할 수 있도록 한다. 사출 성형 프레스는 이러한 패턴을 보여주는데 플라스틱 장난감 제조업체는 플라스틱 성형 분말을 가공하여 원하는 모양의 금형에 플라스틱을 주입한다. 장난감의 등급(자동차, 액션 피규어 등)은 금형에 의해 결정된다.

결과#

  1. 서브클래스에 대한 훅(hook) 메서드를 제공한다.

  2. 병렬적인 클래스 계통을 연결하는 역할을 담당한다.

from abc import ABC, abstractmethod


class Creator(ABC):
    """
    Product 유형의 개체를 반환하는 팩토리 메서드를 선언
    Creator는 기본 ConcreteProduct 객체를 반환하는 팩토리 메서드의 기본 구현을 정의 가능
    Factory 메서드를 호출하여 Product 개체 생성
    """
    def __init__(self):
        self.product = self._factory_method()

    @abstractmethod
    def _factory_method(self):
        pass

    def some_operation(self):
        self.product.interface()


class ConcreteCreator1(Creator):
    """
    팩토리 메서드를 재정의(override)하여 ConcreteProduct1의 인스턴스를 반환
    """
    def _factory_method(self):
        return ConcreteProduct1()


class ConcreteCreator2(Creator):
    """
    팩토리 메서드를 재정의(override)하여 ConcreteProduct2의 인스턴스를 반환
    """
    def _factory_method(self):
        return ConcreteProduct2()


class Product(ABC):
    """
    팩토리 메소드가 생성하는 객체의 인터페이스를 정의
    """
    @abstractmethod
    def interface(self):
        pass


class ConcreteProduct1(Product):
    """Product 인터페이스 구현"""
    def interface(self):
        pass


class ConcreteProduct2(Product):
    """Product 인터페이스 구현"""
    def interface(self):
        pass


def main():
    concrete_creator = ConcreteCreator1()
    concrete_creator.product.interface()
    concrete_creator.some_operation()

빌더#

의도#

빌더(builder)는 복합 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 패턴이다. 빌더는 호출될 때마다 복잡한 개체의 일부를 만들고 모든 중간 상태를 유지한다. 제품이 완료되면 클라이언트는 빌더에서 결과를 검색한다. 구축 프로세스를 보다 세밀하게 제어할 수 있다. 한 번에 제품을 구성하는 창작 패턴과 달리 빌더 패턴은 “감독”의 통제하에 제품을 단계별로 구성한다.

활용성#

  • 복잡 객체의 생성 알고리즘이 이를 합성하는 요소 객체들이 무엇인지 이들의 조립 방법에 독립적일 때

  • 합성할 객체들의 표현이 서로 다르더라도 생성 절차에서 이를 지원해야 할 때

구조#

Reader는 공통 입력의 구문 분석을 캡슐화한다. 빌더 계층 구조는 많은 독특한 표현이나 대상의 다형성 생성을 가능하게 한다.

빌더 패턴은 복잡한 객체의 구성을 해당 표현과 분리하여 동일한 구성 프로세스가 다른 표현을 생성할 수 있도록 한다. 이 패턴은 패스트푸드점에서 어린이 식사를 구성하는 데 사용된다. 아이들의 식사는 일반적으로 메인, 사이드, 음료, 장난감(햄버거, 감자튀김, 콜라, 공룡장난감 등)으로 구성되어 있다. 어린이 제품의 내용에 차이가 있을 수 있지만 구성 과정은 동일할 것이다. 고객이 햄버거, 치즈버거 또는 치킨을 주문하든 프로세스는 동일하다. 카운터 직원은 동료에게 메인 아이템, 사이드 아이템, 장난감을 조립하도록 지시한다. 그런 다음 이러한 항목을 포장한다. 음료는 컵에 담겨 포장된 제품과 따로 제공한다.

결과#

  1. 제품에 대한 내부 표현을 다양하게 변화할 수 있다.

  2. 생성과 표현에 필요한 코드를 분리한다.

  3. 복합 개체를 생성하는 절차를 좀더 세밀하게 나눌 수 있다.

from abc import ABC, abstractmethod


class Director:
    """Builder 인터페이스를 사용하여 객체 생성"""
    def __init__(self):
        self._builder = None

    def construct(self, builder):
        self._builder = builder
        self._builder._build_part_a()
        self._builder._build_part_b()
        self._builder._build_part_c()


class Builder(ABC):
    """
    Product 개체의 일부를 만들기 위한 추상 인터페이스를 지정
    """
    def __init__(self):
        self.product = Product()

    @abstractmethod
    def _build_part_a(self):
        pass

    @abstractmethod
    def _build_part_b(self):
        pass

    @abstractmethod
    def _build_part_c(self):
        pass


class ConcreteBuilder(Builder):
    """
    Builder 인터페이스를 구현하여 제품의 일부를 구성하고 조합
    생성된 표현을 정의하고 추적, 제품 검색을 위한 인터페이스 제공
    """
    def _build_part_a(self):
        pass

    def _build_part_b(self):
        pass

    def _build_part_c(self):
        pass


class Product:
    """구성 중인 복잡한 객체를 표현"""
    pass


def main():
    concrete_builder = ConcreteBuilder()
    director = Director()
    director.construct(concrete_builder)
    product = concrete_builder.product

{ “tags”: [ “hide-cell”, ] }

Hide code cell content
class Cat:
    def __init__(self, height, weight, color):
        self.height = height
        self.weight = weight
        self.color = color

    def print(self):
        return print(f"{self.height}cm, {self.weight}kg, {self.color}")

# cat = Cat(30, 10, 'black')
# cat.print()

class CatBuilder:
    def __init__(self):
        self.height = None
        self.weight = None
        self.color = None

    def setHeight(self, height):
        self.height = height
        return self
    
    def setWeight(self, weight):
        self.weight = weight
        return self

    def setColor(self, color):
        self.color = color
        return self

    def build(self):
        cat = Cat(self.height, self.weight, self.color)
        return cat

# cat_builder = CatBuilder()
# cat_builder.setHeight(30)
# cat_builder.setWeight(7)
# cat_builder.setColor('black')
# cat = cat_builder.build()
# cat.print()

cat = CatBuilder().setHeight(20).setWeight(10).setColor('yellow').build()
cat.print()

class WhiteCatBuilder(CatBuilder):
    def __init__(self):
        super().__init__()
        self.color = 'white'

class BlackCatBuilder(CatBuilder):
    def __init__(self):
        super().__init__()
        self.color = 'black'

white_cat = WhiteCatBuilder().setHeight(10).setWeight(5).build()
black_cat = BlackCatBuilder().setHeight(20).setWeight(10).build()

white_cat.print()
black_cat.print()

class Director:
    def setSmallCat(self, builder:CatBuilder):
        builder.setWeight(5)
        builder.setHeight(5)

    def setBigCat(self, builder:CatBuilder):
        builder.setWeight(100)
        builder.setHeight(50)

director = Director()
cat_builder = BlackCatBuilder()
director.setBigCat(cat_builder)
cat = cat_builder.build()

cat.print()
20cm, 10kg, yellow
10cm, 5kg, white
20cm, 10kg, black
50cm, 100kg, black

프로토타입#

의도#

프로토타입은 생성할 객체의 종류를 명세하는 데에 원형이 되는 예시물을 이용하고, 그 원형을 복사함으로써 새로운 객체를 생성하는 패턴이다. 즉, 미리 만들어진 객체를 복사해서 객체를 생성하는 방식이며 객체를 많이 만들어야 할 경우, 객체 생성에 드는 코딩 분량을 현저히 줄일 수 있다. 순수한 가상 clone 메서드를 지정하는 추상 기본 클래스를 선언하고 모든 cloneable 구체적인 파생 클래스의 사전을 유지 관리한다. “다형성 생성자” 기능이 필요한 모든 클래스: 추상 기본 클래스에서 파생되고, 프로토타입 인스턴스를 등록하고, clone() 작업을 구현한다. 그러면 클라이언트는 고정된 클래스 이름에서 new 연산자를 호출하는 코드를 작성하는 대신 추상 기본 클래스에서 clone 작업을 호출하여 원하는 특정 구체적인 파생 클래스를 지정하는 문자열 또는 열거 데이터 유형을 제공한다.

활용성#

  • 인스턴스화할 클래스를 런타임에 지정할 때(이를테면, 동적 로딩), 또는

  • 제품 클래스 계통과 병렬적으로 만드는 팩토리 클래스를 피하고 싶을 때, 또는

  • 클래스의 인스턴스들이 서로 다른 상태 조합 중에 어느 하나일 때 원형 패턴을 쓴다.

구조#

Factory는 올바른 프로토타입을 찾는 방법으로 각 Product는 자체적으로 새로운 인스턴스를 생성하는 방법을 알고 있다.

프로토타입 패턴은 프로토타입 인스턴스를 사용하여 생성할 객체의 종류를 지정한다. 신제품의 프로토타입은 종종 전체 생산 이전에 만들어지지만 이 예에서 프로토타입은 수동적이며 복제 자체에 참여하지 않는다. 세포의 유사분열(두 개의 동일한 세포가 생성됨)은 자체 복제에 적극적인 역할을 하여 원형 패턴을 보여주는 원형의 한 예시이다. 세포가 분열하면 유전자형이 동일한 두 개의 세포가 생성된다. 즉, 세포는 자신을 복제한다.

결과#

  1. 런타임에 새로운 제품을 추가하고 삭제할 수 있다.

  2. 값들을 다양화함으로써 새로운 객체를 명세한다.

  3. 구조를 다양화함으로써 새로운 객체를 명세할 수 있다.

  4. 서브클래스의 수를 줄인다.

  5. 동적으로 클래스에 따라 응용프로그램을 설정할 수 있다.

import copy


class Prototype:
    """
    Example class to be copied.
    """
    pass


def main():
    prototype = Prototype()
    prototype_copy = copy.deepcopy(prototype)
Hide code cell content
import copy

class Cat:
    def __init__ (self):
        self.color = None
        self.eye_color = None
        self.nose_color = None
        self.tail_color = None
        self.name = None

    def clone(self):
        return copy.deepcopy(self)

# kitty = Cat()
# kitty.color = 'white'
# kitty.eye_color = 'white'
# kitty.nose_color = 'white'
# kitty.tail_color = 'white'
# kitty.name = 'kitty'

# nabi = kitty.clone()
# nabi.name = 'nabi'

class BlackCat(Cat):
    def __init__(self):
        super().__init__()
        self.color = 'black'

class WhiteCat(Cat):
    def __init__(self):
        super().__init__()
        self.color = 'white'

black_cat = BlackCat()
black_cat.nose_color = 'pink'
black_cat.tail_color = 'green'

kitty = black_cat.clone()
kitty.eye_color = 'white'
kitty.name = 'kitty'

nabi = black_cat.clone()
nabi.eye_color = 'blue'
nabi.name = 'nabi'

싱글톤#

의도#

싱글톤(signleton)은 어떤 클래스의 인스턴스는 오직 하나임을 보장하며, 이 인스턴스에 접근할 수 있는 전역적인 접촉점을 제공하는 패턴이다. 이러한 싱글턴 패턴은 오직 유일한 객체를 통해서만 어떤 리소스에 접근해야하는 제약이 있는 상황에서 유용하게 사용할 수 있다. 클래스를 사용하는 입장에서는 실수로 여러 번 객체 생성을 시도하더라도 내부적으로는 오직 하나의 객체만 생성되고 사용된다.

활용성#

  • 클래스의 인스턴스가 오직 하나여야 함을 보장하고, 잘 정의된 접근점(access point)으로 모든 사용자가 접근할 수 있도록 해야 할 때

  • 유일한 인스턴스가 서브클래싱으로 확장되어야 하며, 사용자는 코드의 수정없이 확장된 서브클래스의 인스턴스를 사용할 수 있어야 할 때

구조#

결과#

  1. 유일하게 존재하는 인스턴스로의 접근을 통제한다.

  2. 이름 공간(name space)을 좁힌다.

  3. 연산 및 표현의 정제를 허용한다.

  4. 인스턴스의 개수를 변경하기가 자유롭다.

  5. 클래스 연산을 사용하는 것보다 훨씬 유연한 방법이다.

객체 생성과 초기화#

파이썬에서 객체의 생성과 초기화에는 __new____init__ 이라는 메서드가 사용된다. 여러분은 보통 클래스를 정의할 때 __init__ 만을 주로 사용했을텐데 __new__는 클래스에 정의되어 있지 않으면 알아서 object 클래스의 __new__가 호출되어 객체가 생성된다. 생성된 객체에 속성(property)을 추가할 때 __init__이 호출된다.

클래스를 정의할 때 __new__ 메서드를 작성할 수도 있는데 이 경우 object 클래스의 __new__가 아니라 클래스의 정의된 __new__가 호출되는데 이를 오버라이드(override) 했다고 표현한다. 사용자가 어떤 목적으로 클래스의 생성 과정에 관여하고 싶을 때 직접 __new__ 메서드를 클래스의 추가함으로써 object 클래스의 __new__가 아니라 사용자가 정의한 __new__ 메서드가 호출되도록 한다.

사용자가 클래스의 __new__ 메서드를 재 정의를 할 때는 사용자가 직접 object 클래스의__new__ 메서드를 호출해서 객체를 생성하고 생성된 객체를 리턴하는 코드를 구현해야합니다. 다음 코드는 Foo라는 클래스를 정의하고 객체를 생성한 예제이다. 객체가 생성될 때 화면에 __new__ is called라는 문자열을 출력해주기 위해 Foo 클래스의 __new__ 메서드를 재 정의했다. Foo 클래스에서 재 정의된 __new__ 메서드에서 Foo 클래스에 대한 객체를 생성해주기 위해 super() 즉, object 클래스의 __new__ 메서드를 호출했고 인자로 cls (여기서는 Foo)를 넘겨줬습니다. 마지막으로 생성된 객체를 리턴해준다.

출처: https://wikidocs.net/69361

class Singleton(type):
    """
    클라이언트가 고유한 인스턴스에 액세스할 수 있도록 하는 인스턴스 작업을 정의
    """
    def __init__(cls, name, bases, attrs, **kwargs):
        super().__init__(name, bases, attrs)
        cls._instance = None

    def __call__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__call__(*args, **kwargs)
        return cls._instance


class MyClass(metaclass=Singleton):
    """
    Example class.
    """

    pass


def main():
    m1 = MyClass()
    m2 = MyClass()
    assert m1 is m2


main()

구조 패턴#

구조 패턴(structural pattern)은 객체 결합으로 프로그램 내의 데이터나 인터페이스의 구조를 설계하는 데 많이 활용할 수 있으며 클래스나 객체의 구성(합성)으로 더 큰 구조를 만들어야 할 때 유용한 디자인 패턴이다. 규모가 큰 시스템은 많은 클래스로 구성되어 있어 구조가 복잡하다. 구조가 복잡하면 수정이 쉽지 않고 수정으로 인한 오류도 많이 발생할 수 있다. 이렇게 복잡한 구조를 갖는 시스템을 개발하기 쉽게 만들어주는 것이 바로 구조 패턴이다. 구조 패턴을 이용하면 새로운 기능을 가진 복합 객체를 효과적으로 생성할 수 있다. 구조 패턴에는 다음과 같은 것이 있다.

이러한 디자인 패턴은 모두 클래스 및 개체 구성에 관한 것이며 구조적 클래스 생성 패턴은 상속을 사용하여 인터페이스를 구성한다. 구조적 객체 패턴은 새로운 기능을 얻기 위해 객체를 구성하는 방법을 정의하는 것이다.

  • 어댑터(adapter) 패턴: 다른 클래스의 인터페이스를 일치함

  • 브리지(bridge) 패턴: 객체의 인터페이스를 자신의 구현과 분리함.

  • 컴포지트(composite) 패턴: 단순 및 복잡 객체의 트리 구조

  • 데코레이터(decorator) 패턴: 동적으로 객체에게 책임을 추가

  • 파사드(facade) 패턴: 전체 하위 시스템을 나타내는 단일 클래스

  • 플라이웨이트(flyweight) 패턴: 효율적인 공유를 위해 사용되는 세분화된 개체

  • 프록시(proxy) 패턴: 또 다른 객체를 나타내는 객체

어댑터#

의도#

어댑터(adapter)는 클래스의 인터페이스를 사용자가 기대하는 다른 인터페이스로 변환하는 패턴으로, 호환성이 없는 인터페이스 때문에 함께 동작할 수 없는 클래스들이 함께 작동하도록 하는 것이다. 어떤 클래스를 우리가 바로 사용할 수 없을 때가 있다. 다른 곳에서 개발한 클래스고, 우리가 그것을 수정할 수 없을 때, 우리에게 맞게 중간에 변환할 역할을 해줄 수 있는 클래스가 필요한데, 그것이 바로 어댑터이다. 즉, 클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환한다. 어댑터를 사용하면 호환되지 않는 인터페이스로 인해 할 수 없었던 클래스가 함께 작동할 수 있다. 따라서 기존 클래스를 새 인터페이스로 래핑하고 임피던스는 기존 구성 요소를 새 시스템에 일치시킨다.

활용성#

  • 기존 클래스를 사용하고 싶은데 인터페이스가 맞지 않을 때

  • 아직 예측하지 못한 클래스나 실제 관련되지 않는 클래스들이 기존 클래스를 재사용하고자 하지만, 이미 정의된 재사용 가능한 클래스가 지금 요청하는 인터페이스를 꼭 정의하고 있지 않을 때.

  • 이미 존재하는 여러 개의 서브클래스를 사용해야 하는데, 이 서브클래스들의 상속을 통해서 이들의 인터페이스를 다 개조한다는 것이 현실성이 없을 때. 객체 적응자를 써서 부모 클래스의 인터페이스를 변형하는 것이 더 바람직함.

구조#

아래에서 레거시 Rectangle 구성 요소의 display() 메서드는 “x, y, w, h” 매개변수를 받는다. 그러나 클라이언트는 “왼쪽 위 x 및 y”와 “오른쪽 아래 x 및 y”를 전달한다. 이런 매개변수 불일치는 간접 참조 수준을 추가하여 조정할 수 있으며 이를 어댑터 객체라고 한다.

어댑터는 “래퍼”로 생각할 수도 있다.

“기성품” 구성 요소는 재사용하고 싶은 강력한 기능을 제공하지만 해당 “관점”은 현재 개발 중인 시스템의 철학 및 아키텍처와 호환되지 않는다. 어댑터 패턴을 사용하면 한 클래스의 인터페이스를 클라이언트가 예상하는 인터페이스로 변환하여 호환되지 않는 클래스가 함께 작동할 수 있다. 소켓 렌치는 어댑터의 예를 제공한다. 드라이브의 크기가 동일하다면 소켓은 래칫에 부착된다. 미국의 일반적인 드라이브 크기는 1/2” 및 1/4”입니다. 분명히 1/2” 드라이브 래칫은 어댑터를 사용하지 않는 한 1/4” 드라이브 소켓에 맞지 않는다. 1/2” ~ 1/4” 어댑터에는 1/2” 드라이브 래칫에 맞는 1/2” 암 연결부와 1/4” 드라이브 소켓에 맞는 1/4” 수 연결부가 있다.

from abc import ABC, abstractmethod


class Target(ABC):
    """
    클라이언트가 사용하는 도메인별 인터페이스 정의
    """
    def __init__(self):
        self._adaptee = Adaptee()

    @abstractmethod
    def request(self):
        pass


class Adapter(Target):
    """
    Adapter의 인터페이스를 Target 인터페이스에 맞춤
    """

    def request(self):
        self._adaptee.specific_request()


class Adaptee:
    """
    조정이 필요한 기존 인터페이스 정의
    """
    def specific_request(self):
        pass


def main():
    adapter = Adapter()
    adapter.request()


main()
Hide code cell content
class Animal:     #interface class
    def walk(self):
        pass

class Cat(Animal):
    def walk(self):
        print("cat walking")

class Dog(Animal):
    def walk(self):
        print("dog walking")

def makeWalk(animal : Animal):
    animal.walk()

kitty = Cat()
bingo = Dog()

makeWalk(kitty)
makeWalk(bingo)
cat walking
dog walking

브리지#

의도#

브리지(bridge)는 구현부에서 추상층을 분리하여 각자 독립적으로 변형할 수 있게 하는 패턴이다. 즉, 기능과 구현에 대해 두 개의 별도의 클래스로 구현한다. 브리지 패턴과 어댑터 패턴의 차이점은 어댑터 패턴은 시스템의 설계까 완료된 후 특정 요구 사항때문에 어댑터 패턴을 적용하지만 브리지 패턴은 설계 진행 중에 의도적으로 적용시켜 두 레이어로 분리시키는 것이다.

활용성#

  • 추상적 개념과 이에 대한 구현 사이의 지속적인 종속 관계를 피하고 싶을 때. 이를테면, 런타임에 구현 방법을 선택하거나 구현 내용을 변경하고 싶을 때가 여기에 해당된다.

  • 추상적 개념과 구현 모두가 독립적으로 서브클래싱을 통해 확장되어야 할 때. 이때, 브리지 패턴은 개발자가 구현을 또 다른 추상적 개념과 연결할 수 있게 할 뿐 아니라, 각각을 독집적으로 확장 가능하게 한다.

  • 추상적 개념에 대한 구현 내용을 변경하는 것이 다른 관련 프로그램에 아무런 영향을 주지 않아야 할 때. 즉, 추상적 개념에 해당하는 클래스를 사용하는 코드들은 구현 클래스가 변경되었다고 해서 다시 컴파일되지 않아야 한다.

구조#

Bridge 패턴은 추상화를 구현과 분리하여 두 가지가 독립적으로 변할 수 있도록 한다. 조명, 천장 선풍기 등을 제어하는 가정용 스위치가 Bridge의 한 예시이다. 스위치의 목적은 장치를 켜거나 끄는 것으로, 실제 스위치는 풀 체인, 단순한 2위치 스위치 또는 다양한 조광 스위치로 구현할 수 있다.

결과#

  1. 인터페이스와 구현 분리: 구현이 인터페이스 얾매이지 않게 된다.

  2. 확장성 제고: 추상화와 구현부를 독립적으로 확장할 수 있다.

  3. 구현 세부 사항을 사용자에게서 숨기기

from abc import ABC, abstractmethod


class Abstraction:
    """
    추상화의 인터페이스를 정의하고, Implementor 유형의 객체에 대한 참조를 유지 관리함.
    """

    def __init__(self, imp):
        self._imp = imp

    def operation(self):
        self._imp.operation_imp()


class Implementor(ABC):
    """
    구현 클래스에 대한 인터페이스를 정의하고 이 인터페이스는 Abstraction의
    인터페이스와 정확히 일치할 필요는 없음. 일반적으로 Implementor 인터페이스는 
    기본 작업만 제공하고 추상화는 이러한 기본 작업을 기반으로 상위 수준 작업을 정의함.
    """

    @abstractmethod
    def operation_imp(self):
        pass


class ConcreteImplementorA(Implementor):
    """
    Implementor 인터페이스를 구현하고 구체적인 구현을 정의함.
    """

    def operation_imp(self):
        pass


class ConcreteImplementorB(Implementor):
    """
    Implementor 인터페이스를 구현하고 구체적인 구현을 정의함.
    """
    def operation_imp(self):
        pass


def main():
    concrete_implementor_a = ConcreteImplementorA()
    abstraction = Abstraction(concrete_implementor_a)
    abstraction.operation()
Hide code cell content
from abc import ABC, abstractmethod

class ExerciseHandler(ABC):
    @abstractmethod
    def warm_up(self):
        ...
        
    @abstractmethod
    def exercise(self):
        ...
        
    @abstractmethod
    def cool_down(self):
        ...
        

class LowerBodyMethod(ExerciseHandler):
    def warm_up(self):
        print('하체를 열심히 달군다.')
        
    def exercise(self):
        print('본세트를 임하여 걷지 못할 정도로 열심히 하체를 찢는다.')
        
    def cool_down(self):
        print('달군 하체를 차분히 식혀준다.')
        
        
class UpperBodyMethod(ExerciseHandler):
    def warm_up(self):
        print('상체를 열심히 달군다.')
        
    def exercise(self):
        print('본세트를 임하여 손이 들리지 않을 정도로 열심히 상체를 찢는다.')
        
    def cool_down(self):
        print('달군 상체를 차분히 식혀준다.')
        
        
class Exerciser:
    def __init__(self, handler):
        self.handler = handler

    def warm_up(self):
        self.handler.warm_up(self)
        
    def exercise(self):
        self.handler.exercise(self)
        
    def cool_down(self):
        self.handler.cool_down(self)
        
    def start(self):
        raise NotImplementedError
        
        
class OnlyLower(Exerciser):
    def __init__(self, method):
        super().__init__(method)
    
    def start(self):
        print('하체운동은 어떻게?')
        

class OnlyUpper(Exerciser):
    def __init__(self, method):
        super().__init__(method)
    
    def start(self):
        print('상체운동은 어떻게?')
        
        
only_lower = OnlyLower(LowerBodyMethod)
only_upper = OnlyUpper(UpperBodyMethod)

only_lower.start()
only_lower.warm_up()
only_lower.exercise()

only_upper.start()
only_upper.exercise()
only_upper.exercise()
only_upper.exercise()
하체운동은 어떻게?
하체를 열심히 달군다.
본세트를 임하여 걷지 못할 정도로 열심히 하체를 찢는다.
상체운동은 어떻게?
본세트를 임하여 손이 들리지 않을 정도로 열심히 상체를 찢는다.
본세트를 임하여 손이 들리지 않을 정도로 열심히 상체를 찢는다.
본세트를 임하여 손이 들리지 않을 정도로 열심히 상체를 찢는다.

컴포지트#

의도#

컴포지트(composite, 복합체)는 객체들의 관계를 트리 구조로 구성하여 부분-전체 계층을 표현하는 패턴으로 사용자가 단일 객체와 복합 객체 모두 동일하게 다루도록하는 패턴이다. 이를 재귀적으로 구성하고 일대다 “has a” 위의 “is a” 계층구조이다.

활용성#

  • 부분-전체의 객체 계통을 표현하고 싶을 때

  • 사용자가 객체의 합성으로 생긴 복합 객체와 개개의 객체 사이의 차이를 알지 않고도 자기 일을 할 수 있도록 만들고 싶을 때. 사용자는 복합 구조(composite structure)의 모든 객체를 똑같이 취급하게 된다.

구조#

Components를 포함하는 Composites로, 각 구성요소는 Composite일 수 있습니다.

Composite는 개체를 트리 구조로 구성하고 클라이언트가 개별 개체와 컴포지션을 균일하게 처리할 수 있도록 합니다. 예제는 추상적이지만 산술 표현식은 복합입니다. 산술식은 피연산자, 연산자(+ - * /) 및 다른 피연산자로 구성됩니다. 피연산자는 숫자이거나 다른 산술 표현식일 수 있습니다. 따라서 2 + 3 및 (2 + 3) + (4 * 6)은 모두 유효한 표현식입니다.

결과#

  1. 기본 객체와 복합 객체로 구성된 하나의 일관된 클래스 계통을 정의한다.

  2. 사용자의 코드가 단순해진다. 사용자 코드는 복합 구조이나 단일 객체와 동일하게 다루는 코드로 작성되기 때문이다.

  3. 새로운 종류의 구성요소를 쉽게 추가할 수 있다.

from abc import ABC, abstractmethod


class Component(ABC):
    """
    컴포지션의 개체에 대한 인터페이스를 선언 그리고 모든 클래스에 
    공통적인 인터페이스에 대한 기본 동작을 적절하게 구현함.
    자식 구성 요소에 액세스하고 관리하기 위한 인터페이스를 선언함.
    재귀 구조에서 구성 요소의 부모에 액세스하기 위한 인터페이스를
    정의하고 적절한 경우 구현함(선택 사항).
    """
    @abstractmethod
    def operation(self):
        pass


class Composite(Component):
    """
    자식이 있는 구성 요소의 동작을 정의하고 자식 구성 요스를 저장함.
    Component 인터페이스에서 자식 관련 작업을 구현함.
    """
    def __init__(self):
        self._children = set()

    def operation(self):
        for child in self._children:
            child.operation()

    def add(self, component):
        self._children.add(component)

    def remove(self, component):
        self._children.discard(component)


class Leaf(Component):
    """
    컴포지션에서 잎 개체를 나타내며 잎에는 자식이 없음. 
    컴포지션에서 기본 개체의 동작을 정의함.
    """
    def operation(self):
        pass


def main():
    leaf = Leaf()
    composite = Composite()
    composite.add(leaf)
    composite.operation()
Hide code cell content
class Component:
    def fn(self):
        pass

class Leaf(Component):
    def fn(self):
        print('leaf')

class Composite(Component):
    def __init__(self):
        self.components = []

    def add(self, component:Component):
        self.components.append(component)

    def fn(self):
        print('composite')
        for component in self.components:
            component.fn()

compost1 = Composite()
compost1.add(Leaf())
compost1.add(Leaf())

compst0 = Composite()
compst0.add(Leaf())
compst0.add(compost1)

compst0.fn()

class Animal:
    def speak(self):
        pass

class Cat(Animal):
    def speak(self):
        print('meow')

class Dog(Animal):
    def speak(self):
        print('bark')

class AnimalGroup(Animal):
    def __init__(self):
        self.animals = []
    
    def add(self, animal:Animal):
        self.animals.append(animal)

    def speak(self):
        print("group speaking..")
        for animal in self.animals:
            animal.speak()

cat_group = AnimalGroup()
cat_group.add(Cat())
cat_group.add(Cat())
cat_group.add(Cat())

dog_group = AnimalGroup()
dog_group.add(Dog())
dog_group.add(Dog())

zoo = AnimalGroup()
zoo.add(cat_group)
zoo.add(dog_group)

zoo.speak()
composite
leaf
composite
leaf
leaf
group speaking..
group speaking..
meow
meow
meow
group speaking..
bark
bark

데코레이터#

의도#

데코레이터(decorator)는 주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 패턴으로, 기능 확장이 필요할 때 서브클래싱 대신 쓸 수 있는 유연한 대안이 될 수 있다.

활용성#

  • 동적으로 또한 투명하게(transparent), 다시 말해 다른 객체에 영향을 주지 않고 개개의 객체에 새로운 책임을 추가하기 위해 사용한다.

  • 제거될 수 있는 책임에 사용한다.

  • 실제 상속으로 서브클래스를 계속 만드는 방법이 실질적이지 못할 때 사용한다. 너무 많은 수의 독립된 확장이 가능할 때 모든 조합을 지원하기 위해 이를 상속으로 해결하면 클래스 수가 폭발적으로 많아지게 된다. 아니면, 클래스 정의가 숨겨지든가, 그렇지 않더라도 서브클래싱을 할 수 없게 된다.

구조#

데코레이터는 객체에 동적으로 추가 책임을 부여할 수 있다. 소나무나 전나무에 추가되는 장식품은 데코레이터의 예이다. 조명, 화환, 사탕 지팡이, 유리 장식품 등을 나무에 추가하여 크리스마스 느낌을 줄 수 있다. 어떤 장식품을 사용해도 크리스마스 트리로 인식되는 트리 자체는 바뀌지 않습니다. 한 가지 다른 예로는 돌격소총은 그 자체로 치명적인 무기이지만 특정 “장식”을 적용하면 더 정확하고 조용하며 파괴적으로 만들 수 있다.

결과#

  1. 단순한 상속보다 설계의 융통성을 더 많이 증대시킬 수 있다.

  2. 클래스 계통의 상부측 클래스에 많은 기능이 누적되는 상황을 피할 수 있다.

  3. 장식자와 해당 장식자의 구성요소가 동일한 것은 아니다.

  4. 데코레이터를 사용함으로써 작은 규모의 객체들이 많이 생긴다.

from abc import ABCMeta, abstractmethod


class Component(metaclass=abc.ABCMeta):
    """
    책임을 동적으로 추가할 수 있는 객체에 대한 인터페이스를 정의함.
    """
    @abstractmethod
    def operation(self):
        pass


class Decorator(Component, metaclass=ABCMeta):
    """
    Component 객체에 대한 참조를 유지하고 Component의 
    인터페이스를 준수하는 인터페이스를 정의함.
    """
    def __init__(self, component):
        self._component = component

    @abstractmethod
    def operation(self):
        pass


class ConcreteDecoratorA(Decorator):
    """
    구성 요소에 책임을 추가함.
    """
    def operation(self):
        # ...
        self._component.operation()
        # ...


class ConcreteDecoratorB(Decorator):
    """
    구성 요소에 책임을 추가함.
    """
    def operation(self):
        # ...
        self._component.operation()
        # ...


class ConcreteComponent(Component):
    """
    추가 책임이 첨부될 수 있는 개체를 정의함.
    """
    def operation(self):
        pass


def main():
    concrete_component = ConcreteComponent()
    concrete_decorator_a = ConcreteDecoratorA(concrete_component)
    concrete_decorator_b = ConcreteDecoratorB(concrete_decorator_a)
    concrete_decorator_b.operation()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[14], line 4
      1 from abc import ABCMeta, abstractmethod
----> 4 class Component(metaclass=abc.ABCMeta):
      5     """
      6     책임을 동적으로 추가할 수 있는 객체에 대한 인터페이스를 정의함.
      7     """
      8     @abstractmethod
      9     def operation(self):

NameError: name 'abc' is not defined

파사드#

의도#

파사드(facade)는 서브시스템이 있는 인터페이스 집합에 대해서 하나의 통합된 인터페이스를 제공하는 패턴으로, 서브시스템을 좀더 사용하기 편하게 만드는 상위 수준의 인터페이스를 정의한다. 대규모 프로그램에는 서로 관련있는 클래스들이 많다. 복잡하게 얽혀있는 클래스들을 정리해서 높은 레벨의 인터페이스(API)를 제공하는 것이다. 여러 클래스들을 직접 제어하지 않고 ‘창구(facade)’에 요구한다. 즉, 여러 클래스들의 기능들을 묶은 Facade 클래스를 만들고 Facade 클래스에 접근하게 구현한다.

활용성#

  • 복잡한 서브시스템에 대한 단순한 인터페이스 제공이 필요할 때

  • 추상 개념에 대한 구현 클래스와 사용자 사이에 너무 많은 종속성이 존재할 때

  • 서브시스템을 계층화시킬 때. 퍼사드 패턴을 사용하여 각 서브시스템의 계층에 대한 접근점을 제공한다.

구조#

Facade는 사용하기 쉽게 하는 하위 시스템에 대한 통합된 상위 수준 인터페이스를 정의한다. 소비자는 카탈로그에서 주문할 때 Facade를 접하게 되며하나의 번호로 전화를 걸어 고객 서비스 담당자와 상담한다. 고객 서비스 담당자는 Facade 역할을 하여 주문 이행 부서, 청구 부서 및 배송 부서에 대한 인터페이스를 제공합니다.

결과#

  1. 서브시스템의 구성요소를 보호할 수 있다. 이로써 사용자가 다루어야 할 객체의 수가 줄어들며, 서브시스템을 쉽게 사용할 수 있다.

  2. 서브시스템과 사용자 코드 간의 결합도를 더욱 약하게 만든다.

  3. 응용프로그램 쪽에서 서브시스템 클래스를 사용하는 것을 완전히 막지 않는다. 그러므로 파사드를 사용할지 서브시스템 클래스를 직접 사용할지 결정할 수 있다.

class Facade:
    """
    Know which subsystem classes are responsible for a request.
    Delegate client requests to appropriate subsystem objects.
    """
    def __init__(self):
        self._subsystem_1 = Subsystem1()
        self._subsystem_2 = Subsystem2()

    def operation(self):
        self._subsystem_1.operation1()
        self._subsystem_1.operation2()
        self._subsystem_2.operation1()
        self._subsystem_2.operation2()


class Subsystem1:
    """
    Implement subsystem functionality.
    Handle work assigned by the Facade object.
    Have no knowledge of the facade; that is, they keep no references to
    it.
    """
    def operation1(self):
        pass

    def operation2(self):
        pass


class Subsystem2:
    """
    Implement subsystem functionality.
    Handle work assigned by the Facade object.
    Have no knowledge of the facade; that is, they keep no references to
    it.
    """
    def operation1(self):
        pass

    def operation2(self):
        pass


def main():
    facade = Facade()
    facade.operation()

플라이웨이트#

의도#

플라이웨이트(flyweight)는 크기가 작은 객체가 여러 개 있을 때, 공유를 통해 이들을 효율적으로 지원하는 패턴이다.

활용성#

  • 응용프로그램이 대량의 객체를 사용해야 할 때

  • 객체의 수가 너무 많아져 저장 비용이 너무 높아질 때

  • 대부분의 객체 상태를 부가적인 것으로 만들 수 있을 때

  • 부가적인 속성들을 제거한 후 객체들을 조사해 보니 객체의 많은 묶음이 비교적 적은 수의 공유된 객체로 대체될 수 있을 때

  • 응용프로그램이 객체의 정체성에 의존하지 않을 때. 플라이웨이트 객체들은 공유될 수 있음을 의미하는데, 식별자가 있다는 것은 서로 다른 객체로 구별해야 한다는 의미이므로 플라이웨이트를 사용할 수 없다.

구조#

플라이웨이트는 공유를 사용하여 많은 수의 객체를 효율적으로 지원한다. 최신 웹 브라우저는 이 기술을 사용하여 동일한 이미지를 두 번 로드하는 것을 방지한다. 브라우저가 웹 페이지를 로드하면 해당 페이지의 모든 이미지를 탐색하고 브라우저는 인터넷에서 모든 새 이미지를 로드하고 내부 캐시에 저장한다. 이미 로드된 이미지의 경우 페이지 내 위치와 같은 고유한 데이터가 있는 플라이웨이트 개체가 생성되지만 다른 모든 것은 캐시된 이미지를 참조한다.

결과#

  1. 공유해야 하는 인스턴스의 전체 수를 줄일 수 있다.

  2. 객체별 본질적 상태의 양을 줄일 수 있다.

  3. 부가적인 상태는 연산되거나 저장될 수 있다.

from abc import ABC, abstractmethod


class FlyweightFactory:
    """
    Create and manage flyweight objects.
    Ensure that flyweights are shared properly. When a client requests a
    flyweight, the FlyweightFactory object supplies an existing instance
    or creates one, if none exists.
    """
    def __init__(self):
        self._flyweights = {}

    def get_flyweight(self, key):
        try:
            flyweight = self._flyweights[key]
        except KeyError:
            flyweight = ConcreteFlyweight()
            self._flyweights[key] = flyweight
        return flyweight


class Flyweight(ABC):
    """
    Declare an interface through which flyweights can receive and act on
    extrinsic state.
    """
    def __init__(self):
        self.intrinsic_state = None

    @abstractmethod
    def operation(self, extrinsic_state):
        pass


class ConcreteFlyweight(Flyweight):
    """
    Implement the Flyweight interface and add storage for intrinsic
    state, if any. A ConcreteFlyweight object must be sharable. Any
    state it stores must be intrinsic; that is, it must be independent
    of the ConcreteFlyweight object's context.
    """
    def operation(self, extrinsic_state):
        pass


def main():
    flyweight_factory = FlyweightFactory()
    concrete_flyweight = flyweight_factory.get_flyweight("key")
    concrete_flyweight.operation(None)

프록시 패턴#

의도#

프록시(proxy)는 어떤 다른 객체로 접근하는 것을 통제하기 위해서 그 객체의 대리자(surrogate) 또는 자리채움자(placeholder)를 제공하는 패턴이다.

활용성#

  • 가상 프록시(virtual proxy)는 요청이 잇을 때만 필요한 고비용 객체를 생성한다.

  • 보호용 프록시(protection proxy)는 원래 객체에 대한 실제 접근을 제어한다. 이는 객체별 접근 제어 권한이 다를 때 유용하게 사용할 수 있다.

  • 스마트 참조자(smart reference)는 원시 포인터의 대체용 객체로, 실제 객체에 접근이 일언라 때 추가적인 해동을 수행한다.

구조#

프록시는 개체에 대한 액세스를 제공하기 위해 대리 또는 자리 표시자를 제공합니다. 수표 또는 은행 환어음은 계좌의 자금을 대신합니다. 구매 시 현금 대신 수표를 사용할 수 있으며 궁극적으로 발행인 계정의 현금 액세스를 통제합니다.

결과#

  • 원격지 프록시는 객체가 다른 주소 공간에 존재한다는 사실을 숨길 수 있다.

  • 가상 프록시는 요구에 따라 객체를 생성하는 등 처리를 최적화할 수 있다.

  • 보호용 프록시 및 스마트 참조자는 객체가 접근할 때마다 추가 관리를 책임진다. 객체를 생성할 것인지 삭제할 것인지를 관리한다.

from abc import ABC, abstractmethod


class Subject(ABC):
    """
    Define the common interface for RealSubject and Proxy so that a
    Proxy can be used anywhere a RealSubject is expected.
    """

    @abstractmethod
    def request(self):
        pass


class Proxy(Subject):
    """
    Maintain a reference that lets the proxy access the real subject.
    Provide an interface identical to Subject's.
    """

    def __init__(self, real_subject):
        self._real_subject = real_subject

    def request(self):
        # ...
        self._real_subject.request()
        # ...


class RealSubject(Subject):
    """
    Define the real object that the proxy represents.
    """

    def request(self):
        pass


def main():
    real_subject = RealSubject()
    proxy = Proxy(real_subject)
    proxy.request()

행위 패턴#

객체는 속성(attribute)과 행위(behavior)를 가지고 있고 행위는 객체가 제공하는 기능을 말한다. 따라서 기능을 제공하고 다른 객체의 기능을 사용하기 위해 메시지를 통한 객체 간의 상호작용이 빈번히 일어난다. 행위 패턴(behaviorl pattern)은 반복적으로 사용되는 객체 간의 상호작용을 패턴화한 것으로 클래스나 객체가 상호작용하는 방법과 책임을 분산하는 방법을 정의하한다.

객체의 기능은 변하지 않지만 일을 처리하는 방법이 달라질 때가 있다. 응용 분야에 따라 행위가 다른 객체로 옮겨 가거나 알고리즘이 대체되는 경우이다. 그런 경우 대부분 상속 개념을 사용할 수 있다. 그러나 행위 패턴은 여러 가지 행위 관련 패턴을 사용해 독립적으로 일을 처리할 때 사용한다. 행위 패턴은 메시지 교환과 관련된 것으로 객체 간의 행위나 알고리즘 등 과 관련된 패턴이다. 행위 패턴에는 다음과 같은 것이 있다.

이러한 디자인 패턴은 모두 클래스의 개체 통신에 관한 것입니다. 행동 패턴은 객체 간의 통신과 가장 구체적으로 관련된 패턴이다.

  • 책임 체인(chain of responsibility) 패턴: 객체의 연쇄 간에 요청을 전달하는 방법

  • 커맨드(command) 패턴: 명령 요청을 객체로 캡슐화

  • 인터프리터(interpreter) 패턴: 프로그램에 언어 요소를 포함하는 방법

  • 반복자(iterator) 패턴: 컬렉션의 요소에 순차적으로 접근(access)함.

  • 중재자(mediator) 패턴: 클래스 간의 단순화된 통신을 정의함.

  • 메멘토(memento) 패턴: 객체의 내부 상태를 캡처(capture)하고 복원함.

  • 옵저버(observer) 패턴: 여러 클래스의 변경을 알리는 방법

  • 상태(state) 패턴: 상태가 변경되면 객체의 동작 변경(Alter an object’s behavior when its state changes)

  • 전략(strategy) 패턴: 클래스 내부의 알고리즘을 캡슐화 함.

  • 템플릿 메서드(template method) 패턴: 알고리즘의 정확한 단계를 하위 클래스로 연기함.

  • 방문자(visitor) 패턴: 변경 없이 클래스에 새 작업을 정의함.

책임 연쇄#

의도#

책임 연쇄(chain of responsibility)는 요청을 처리할 수 있는 기회를 하나 이상의 객체에게 부여하여 요청을 보내는 객체와 그 요청을 받는 개체 사이의 결합을 피하는 패턴이다. 요청을 받을 수 있는 객체를 연쇄적으로 묶고, 실제 요청을 처리할 객체를 만날 때까지 고리를 따라서 요청을 전달한다. 어떤 요구가 발생했을 때, 그 요구를 처리할 객체를 바로 결정할 수 없을 때, 다수의 객체를 Chain으로 연결해 차례로 방문하면서 목적에 맞는 객체를 하는 것이다. 요구하는 측과 처리하는 측의 연결을 약화시켜 Coupling을 낮추는 역할을 한다. 가능한 많은 핸들러가 포함된 단일 처리 파이프라인으로 시작 및 종료 요청한다.

활용성#

  • 하나 이상의 객체가 요청을 처리해야 하고, 그 요청 처리자 중 어떤 것이 선행자(priori)인지 모를 때. 처리자가 자동으로 확정되어야 한다.

  • 메시지를 받을 객체를 명시하지 않은 채 여러 객체 중 하나에게 처리를 요청하고 싶을 때

  • 요청을 처리할 수 있는 객체 집합이 동적으로 정의되어야 할 때

구조#

Chain of Responsibility 패턴은 하나 이상의 객체가 요청을 처리할 수 있는 기회를 제공하여 요청의 발신자와 수신자의 연결을 방지한다. ATM은 돈을 주는 메커니즘에서 Chain of Responsibility를 사용한다.

결과#

  • 객체 간의 행동적 결합도가 적어진다.

  • 객체에게 책임을 할당하는 데 유연성을 높일 수 있다.

  • 메시지 수신이 보장되지는 않는다.

from abc import ABC, abstractmethod


class Handler(ABC):
    """
    Define an interface for handling requests.
    Implement the successor link.
    """
    def __init__(self, successor=None):
        self._successor = successor

    @abstractmethod
    def handle_request(self):
        pass


class ConcreteHandler1(Handler):
    """
    Handle request, otherwise forward it to the successor.
    """
    def handle_request(self):
        if True:  # if can_handle:
            pass
        elif self._successor is not None:
            self._successor.handle_request()


class ConcreteHandler2(Handler):
    """
    Handle request, otherwise forward it to the successor.
    """
    def handle_request(self):
        if False:  # if can_handle:
            pass
        elif self._successor is not None:
            self._successor.handle_request()


def main():
    concrete_handler_1 = ConcreteHandler1()
    concrete_handler_2 = ConcreteHandler2(concrete_handler_1)
    concrete_handler_2.handle_request()
class Handler:
    def __init__(self):
        self.next_handler = None
    
    def setNext(self, handler):
        self.next_handler = handler

    def handle(self, req):
        if self.next_handler:
            return self.next_handler.handle(req)
        return None


class CashHandler(Handler):
    def handle(self,req):
        if req['method'] == 'cash':
            print(f"processing cash {req['amount']} won")
        else:
            print(f"CashHandler cannot process")
            super().handle(req)

class CreditCardHandler(Handler):
    def handle(self,req):
        if req['method'] == 'creditCard':
            print(f"processing creditCard {req['amount']} won")
        else:
            print(f"CreditCardHandler cannot process")
            super().handle(req)

class DebitCardHandler(Handler):
    def handle(self,req):
        if req['method'] == 'debitCard':
            print(f"processing debitCard {req['amount']} won")
        else:
            print(f"DebitCardHandler cannot process")
            super().handle(req)

cash_handler = CashHandler()
creditcard_handler = CreditCardHandler()
debitcard_handler = DebitCardHandler()

cash_handler.setNext(creditcard_handler)
creditcard_handler.setNext(debitcard_handler)

payment = {
    'method' : 'paypal',
    'amount' : 10000
}
cash_handler.handle(payment)

커맨드#

의도#

커맨드(command)는 요청을 객체의 형태로 캡슐화하여 서로 요청이 다른 사용자의 매개변수화, 요청 저장 또는 로깅 그리고 연산의 취소를 지원하게 만드는 패턴이다. 실행하고 싶은 메서드의 History (주로 스택으로 저장) 관리가 필요하다. 예를 들면, 우리가 Ctrl + z를 눌러서 실행취소를 한다고 할 때, History를 저장할 수 있어야 가능하다.

활용성#

  • 수행할 동작을 객체로 매개변수화하고자 할 때. 절차 지향 프로그램에서는 이를 콜백(callback) 함수

  • 서로 다른 시간에 요청을 명시하고, 저장하며, 실행하고 싶을 때

  • 실행 취소 기능을 지원하고 싶을 때

  • 시스템이 고장 났을 때 재적용이 가능하도록 변경 과정에 대한 로깅을 지원하고 싶을 때

  • 기본적인 연산의 조합으로 만든 상위 수준 연산을 써서 시스템을 구조화하고 싶을 때

구조#

명령 패턴을 사용하면 요청을 개체로 캡슐화할 수 있으므로 클라이언트가 다른 요청으로 매개변수화될 수 있습니다. 식당에서의 “체크”는 커맨드 패턴의 한 예입니다. 웨이터 또는 웨이트리스는 고객의 주문이나 명령을 받아 수표에 적어 해당 주문을 캡슐화합니다. 그런 다음 주문은 짧은 주문 요리사를 위해 대기합니다. 각 웨이터가 사용하는 “체크” 패드는 메뉴에 종속되지 않으므로 다양한 항목을 요리하는 명령을 지원할 수 있습니다.

결과#

  • 커맨드는 연산을 호출하는 객체와 연산 수행 방법을 구현하는 개체를 분리한다.

  • 커맨드는 일급 클래스이다. 다른 객체와 같은 방식으로 조작되고 확장할 수 있다.

  • 명령 여러 개를 복합해서 복합 명령을 만들 수 있다.

  • 새로운 커맨드 객체를 추가하기 쉽다.

from abc import ABC, abstractmethod


class Invoker:
    """
    Ask the command to carry out the request.
    """
    def __init__(self):
        self._commands = []

    def store_command(self, command):
        self._commands.append(command)

    def execute_commands(self):
        for command in self._commands:
            command.execute()


class Command(ABC):
    """
    Declare an interface for executing an operation.
    """
    def __init__(self, receiver):
        self._receiver = receiver

    @abstractmethod
    def execute(self):
        pass


class ConcreteCommand(Command):
    """
    Define a binding between a Receiver object and an action.
    Implement Execute by invoking the corresponding operation(s) on
    Receiver.
    """
    def execute(self):
        self._receiver.action()


class Receiver:
    """
    Know how to perform the operations associated with carrying out a
    request. Any class may serve as a Receiver.
    """
    def action(self):
        pass


def main():
    receiver = Receiver()
    concrete_command = ConcreteCommand(receiver)
    invoker = Invoker()
    invoker.store_command(concrete_command)
    invoker.execute_commands()
class Command:
    def execute(self):
        pass
    
class PrintCommand(Command):
    def __init__(self, print_str: str):
        self.print_str = print_str
        
    def execute(self):
        print(f"from print command: {self.print_str}")
        
first_command = PrintCommand("first_command")
second_command = PrintCommand("second_command")

first_command.execute()
second_command.execute()

class Dog:
    def sit(self):
        print("The dog sat down")
    
    def stay(self):
        print("The dog is staying")
        
class DogCommand(Command):
    def __init__(self, dog:Dog, commands):
        self.dog = dog
        self.commands = commands
        
    def execute(self):
        for command in self.commands:
            if command == 'sit':
                self.dog.sit()
            elif command == 'stay':
                self.dog.stay()
                
baduk = Dog()
dog_command = DogCommand(baduk, ['stay', 'sit', 'sit'])
dog_command.execute()

class Invoker:
    def __init__(self):
        self.command_list = []
        
    def addCommand(self, command: Command):
        self.command_list.append(command)
        
    def runCommand(self):
        for command in self.command_list:
            command.execute()
            
invoker = Invoker()
invoker.addCommand(first_command)
invoker.addCommand(dog_command)
invoker.addCommand(second_command)

invoker.runCommand()

인터프리터#

의도#

인터프리터(interpreter)는 주어진 언어에 대해 그 언어의 문법을 위한 표현 수단을 정의하고, 이와 아울러 그 표현 수단을 사용하여 해당 언어로 작성된 문장을 해석하는 해석기를 정의하는 패턴이다. 도메인을 언어에 매핑하고 언어를 문법에 매핑하고 문법을 계층적 개체 지향 설계에 매핑한다. 문제 유형은 잘 정의되고 잘 이해된 영역에서 반복적으로 발생하기 때문에 도메인이 “언어”로 특징지어지면 문제는 해석 “엔진”으로 쉽게 해결할 수 있다.

활용성#

  • 정의할 언어의 문법이 간단하다. 문법이 복잡하다면 문법을 정의하는 클래스 계통이 복잡해지고 관리할 수 없게 된다. 이는 인터프리터 패턴을 사용하는 것보다는 파서 생성기와 같은 도구를 이용하는 것이 더 낫다.

  • 효율성은 별로 고려할 사항이 아닐 때, 사실 가장 효율적인 인터프리터를 구현하는 방법은 파스 트리(parse tree)를 직접 해석하도록 만든 것이 아니라, 일차적으로 파스 트리를 다른 형태로 번역(translate)시키는 것이다.

구조#

인터프리터 패턴은 언어에 대한 문법적 표현과 문법을 해석하는 인터프리터를 정의한다. 음악가는 통역사의 예시이다. 소리의 높낮이와 지속 시간은 보표의 악보로 나타낼 수 있으며 이 표기법은 음악의 언어를 제공한다. 악보에서 음악을 연주하는 음악가는 표현된 각 소리의 원래 음높이와 지속 시간을 재현할 수 있다.

결과#

  • 문법의 변경과 확장이 쉽다.

  • 문법의 구현이 용이하다.

  • 복잡한 문법은 관라하기 어렵다.

  • 표현식을 해석하는 새로운 방법을 추가할 수 있다.

from abc import ABC, abstractmethod


class AbstractExpression(ABC):
    """
    Declare an abstract Interpret operation that is common to all nodes
    in the abstract syntax tree.
    """

    @abstractmethod
    def interpret(self):
        pass


class NonterminalExpression(AbstractExpression):
    """
    Implement an Interpret operation for nonterminal symbols in the grammar.
    """

    def __init__(self, expression):
        self._expression = expression

    def interpret(self):
        self._expression.interpret()


class TerminalExpression(AbstractExpression):
    """
    Implement an Interpret operation associated with terminal symbols in
    the grammar.
    """

    def interpret(self):
        pass


def main():
    abstract_syntax_tree = NonterminalExpression(TerminalExpression())
    abstract_syntax_tree.interpret()

반복자#

의도#

반복자(iterator)는 내부 표현부를 노출하지 않고 어떤 객체 집합에 속한 원소들을 순차적으로 접근할 수 있는 방법을 제공하는 패턴이다.

활용성#

  • 객체 내부 표현 방식을 모르고도 집합 객체의 각 원소들에 접근하고 싶을 때

  • 집합 객체를 순회하는 다양한 방법을 지원하고 싶을 때

  • 서로 다른 집합 객체 구조에 대해서도 동일한 방법으로 순회하고 싶을 때

구조#

Iterator는 개체의 기본 구조를 노출하지 않고 집계 개체의 요소에 순차적으로 액세스하는 방법을 제공한다. 파일은 집계 개체로 관리 또는 비서 직원을 통해 파일에 액세스하는 사무실 설정에서 Iterator 패턴은 비서가 Iterator 역할을 하는 것으로 시연된다. 비서의 파일링 시스템을 이해하려고 노력하는 임원의 전제를 중심으로 여러 텔레비전 코미디 희극이 개발되었다. 경영진에게 파일링 시스템은 혼란스럽고 비논리적이지만 비서는 파일에 빠르고 효율적으로 액세스할 수 있다.

초기 텔레비전은 다이얼을 사용해 채널을 변경했으며 채널 서핑을 할 때 시청자는 해당 채널의 수신 여부에 관계없이 각 채널 위치를 통해 다이얼을 움직여야 했다. 최신 텔레비전은 다음 및 이전 버튼이 사용한다. 시청자가 “다음” 버튼을 선택하면 다음으로 조정된 채널이 표시한다. 채널을 탐색할 때 채널 번호는 중요하지 않지만 현재 방영하고 있는 프로그램은 중요하다. 시청자는 한 채널의 프로그램이 관심이 없으면 채널의 번호를 몰라도 다음 채널을 요청할 수 있다.

결과#

  • 집합 객체의 다양한 순회 방법을 제공한다.

  • 반복자는 집합체(aggregate) 클래스의 인터페이스를 단순화한다.

  • 집합 객체에 따라 하나 이상의 순회 방법이 제공될 수 있다.

import collections.abc


class ConcreteAggregate(collections.abc.Iterable):
    """
    Implement the Iterator creation interface to return an instance of
    the proper ConcreteIterator.
    """

    def __init__(self):
        self._data = None

    def __iter__(self):
        return ConcreteIterator(self)


class ConcreteIterator(collections.abc.Iterator):
    """
    Implement the Iterator interface.
    """

    def __init__(self, concrete_aggregate):
        self._concrete_aggregate = concrete_aggregate

    def __next__(self):
        if True:  # if no_elements_to_traverse:
            raise StopIteration
        return None  # return element


def main():
    concrete_aggregate = ConcreteAggregate()
    for _ in concrete_aggregate:
        pass

중재자#

의도#

중재자(mediator)는 한 집합에 속해있는 객체들의 상호작용을 캡슐화하는 객체를 정의하는 패턴이다. 객체들이 직접 서로를 참조하지 않도록 함으로써 객체들 사이의 결합도를 낮추고, 개발자가 객체들의 상호작용을 독립적으로 다양화시킬 수 있게 만든다. 우리는 재사용 가능한 구성 요소를 설계하고 싶지만 잠재적으로 재사용 가능한 조각 간의 종속성은 “스파게티 코드” 현상을 보여준다. 모든 행동을 수행하기 전에 ‘중재자 객체’의 결정이 있어야 하고, 중재자 객체로 프로그램이 수행한다. 각 객체들은 중재자만 알아야 한다. 개체 집합이 상호 작용하는 방식을 캡슐화하는 개체를 정의한다. 중재자(mediator) 패턴은 개체가 서로를 명시적으로 참조하지 않도록 하여 느슨한 결합을 촉진하고 개체의 상호 작용을 독립적으로 변경할 수 있도록 설계한다. 상호 작용하는 피어 간의 다대다 관계를 “전체 개체 상태”로 승격한다.

활용성#

  • 여러 객체가 잘 정의된 형태이기는 하지만 복잡한 상호작용을 가질 때. 객체 간의 의존성이 구조화되지 않으며, 잘 이해하기 어려울 때

  • 한 객체가 다른 객체를 너무 많이 참조하고, 너무 많은 의사소통을 수행해서 그 객체를 재사용하기 힘들 때

  • 여러 클래스에 분산된 행동들이 상속 없이 상황에 맞게 수정되어야 할 때

구조#

중재자는 개체 집합이 상호 작용하는 방식을 제어하는 개체를 정의한다. 동료 개체 간의 느슨한 결합은 동료가 서로가 아니라 중재자와 통신하도록 함으로써 달성할 수 있다. 통제된 공항의 관제탑은 이러한 패턴을 잘 보여준다. 터미널 영역에 접근하거나 출발하는 비행기의 조종사는 서로 명시적으로 통신하기보다는 타워와 통신한다. 어느 비행기가 이륙하거나 착륙할 수 있는지에 대한 제약은 관제탑에서 조정한다. 관제탑이 전체 비행을 제어하지 않는다는 점에 유의하는 것이 중요하고 터미널 영역에서 제약 조건을 적용하기 위해서만 존재한다.

결과#

  • 서브클래싱을 제안하고 Colleague 객체 사이의 종속성을 줄인다.

  • 객체 프로토콜을 단순화하고 객체 간의 협력 방법을 추상화한다.

  • 통제가 집중화된다.

class Mediator:
    """
    Implement cooperative behavior by coordinating Colleague objects.
    Know and maintains its colleagues.
    """
    def __init__(self):
        self._colleague_1 = Colleague1(self)
        self._colleague_2 = Colleague2(self)


class Colleague1:
    """
    Know its Mediator object.
    Communicate with its mediator whenever it would have otherwise
    communicated with another colleague.
    """
    def __init__(self, mediator):
        self._mediator = mediator


class Colleague2:
    """
    Know its Mediator object.
    Communicate with its mediator whenever it would have otherwise
    communicated with another colleague.
    """
    def __init__(self, mediator):
        self._mediator = mediator


def main():
    mediator = Mediator()
class Mediator:
    def notify(self, signal:str):
        pass

class Clock:
    def setMediator(self, mediator:Mediator):
        self.mediator = mediator

    def Alarm(self):
        print('alarm on')
        self.mediator.notify('AlarmOn')

class Light:
    def setMediator(self, mediator:Mediator):
        self.mediator = mediator

    def On(self):
        print('light on')
    
    def Off(self):
        print('light off')
        self.mediator.notify('LightOff')

class Speaker:
    def setMediator(self, mediator:Mediator):
        self.mediator = mediator

    def On(self):
        print('speaker on')
    
    def Off(self):
        print('speaker off')

class HomeMediator(Mediator):
    def __init__(self, clock, light, speaker):
        self.clock = clock
        self.light = light
        self.speaker = speaker

    def notify(self, signal:str):
        if signal == 'AlarmOn':
            self.light.On()
            self.speaker.On()

        elif signal == 'LightOff':
            self.speaker.Off()


clock = Clock()
light = Light()
speaker = Speaker()

mediator = HomeMediator(clock, light, speaker)

clock.setMediator(mediator)
light.setMediator(mediator)
speaker.setMediator(mediator)

clock.Alarm()
light.Off()

메멘토#

의도#

메멘토(memento)는 캡슐화를 위배하지 않은 채로 어떤 객체의 내부 상태를 잡아내고 실체화시켜, 이후에 해당 객체가 그 상태로 되돌아올 수 있도록 하는 패턴이다. “체크 포인트” 기능을 캡슐화하는 마법의 쿠키이며 실행 취소 또는 전체 개체 상태로 롤백을 승격한다. 즉, 메멘토(memento) 패턴은 개체를 이전 상태로 복원하기 위한 패턴이다(예: “실행 취소” 또는 “롤백” 작업).

활용성#

  • 어떤 객체의 상태에 대한 스냅샷(몇 개의 일부)을 저장한 후 나중에 이 상태로 복구해야 할 때

  • 상태를 얻는 데 필요한 직접적인 인터페이스를 두면 그 객체의 구현 세부사항이 드러날 수밖에 없고, 이것으로 객체의 캡슐화가 깨질 때

구조#

Memento는 객체의 내부 상태를 캡처하고 외부화하여 객체가 나중에 해당 상태로 복원될 수 있도록 한다. 이 패턴은 자동차의 드럼 브레이크를 수리하는 DIY 정비공 사이에서 일반적이며 드럼은 양쪽에서 제거되어 오른쪽과 왼쪽 브레이크가 모두 노출된다. 한쪽만 분해되고 다른 쪽은 브레이크 부품이 어떻게 결합되었는지에 대한 기념품 역할을 한다. 한쪽에서 작업이 완료된 후에야 다른 쪽이 분해되고 두 번째 면을 분해하면 첫 번째 면이 메멘토 역할을 한다.

결과#

  • 캡슐화된 경계를 유지할 수 있고 Originator 클래스를 단순화할 수 있다.

  • 메멘토의 사용으로 더 많은 비용이 발생할 수 있고, 메멘토를 관리하는 데 필요한 비용이 숨어있다.

  • 제한 범위 인터페이스와 광범위 인터페이스를 정의해야 한다.

import pickle


class Originator:
    """
    Create a memento containing a snapshot of its current internal
    state.
    Use the memento to restore its internal state.
    """
    def __init__(self):
        self._state = None

    def set_memento(self, memento):
        previous_state = pickle.loads(memento)
        vars(self).clear()
        vars(self).update(previous_state)

    def create_memento(self):
        return pickle.dumps(vars(self))


def main():
    originator = Originator()
    memento = originator.create_memento()
    originator._state = True
    originator.set_memento(memento)
import uuid
from datetime import datetime

class CatMemento:
    def __init__(self, age, height):
        self.uuid = uuid.uuid4()
        self.create_time = datetime.now()
        self.age = age
        self.height = height

class Cat:
    def __init__(self, age,height):
        self.age = age
        self. height = height

    def speak(self):
        print(f'{self.age}year old, {self.height}cm, meow')

    def createMemento(self)->CatMemento:
        cat_memento = CatMemento(self.age, self.height)
        return cat_memento

    def restore(self, memento:CatMemento):
        self.age = memento.age
        self.height = memento.height

cat_history = []
cat = Cat(0, 10)
memento0 = cat.createMemento()
cat_history.append(cat.createMemento())

cat.age = 1
cat.height = 25
cat_history.append(cat.createMemento())

cat.age = 2
cat.height = 50
cat_history.append(cat.createMemento())

cat.speak()
cat.restore(cat_history[0])
cat.speak()

옵저버#

의도#

옵저버(observer, 감시자)는 객체들 사이에 일 대 다의 의존 관계를 정의해 두어, 어떤 객체의 상태가 변할 때 그 객체에 의존성을 가진 다른 객체들이 그 변화를 통지받고 자동으로 갱신될 수 있게 만드는 패턴이다. 예를 들어, MVC패턴에서 Model과 View의 분리하는데 사용한다. 옵저버 패턴은 하나의 개체가 상태를 변경하면 모든 종속 항목이 자동으로 알림을 받고 업데이트되도록 개체 간의 일대다 종속성을 정의한다.

활용성#

  • 어떤 추상 개념이 두 가지 양상을 갖고 하나가 다른 하나에 종속적일 때. 각 양상을 별도의 객체로 캡슐화하여 이들 각각을 재사용할 수 있다.

  • 한 객체에 가해진 변경으로 다른 객체를 변경해야 하고, 프로그래머들은 얼마나 많은 객체들이 변경되어야 하는지 몰라도 될 때

  • 어떤 객체가 다른 객체에 자신의 변화를 통보할 수 있는데, 그 변화에 관심있어 하는 객체들이 누구인지에 대한 가정 없이도 그러한 통보가 될 때

구조#

Observer는 일대다 관계를 정의하여 한 개체의 상태가 변경되면 다른 개체에 자동으로 알림이 전달되고 업데이트한다. 경매는 이러한 패턴을 보여주는데 각 입찰자는 입찰을 나타내는 데 사용되는 번호가 매겨진 패들을 가지고 있다. 경매인은 입찰을 시작하고 패들이 입찰을 수락하기 위해 들어올릴 때 “관찰”하고, 입찰을 수락하면 입찰 가격이 변경되며 이는 모든 입찰자에게 새 입찰 형식으로 전달한다.

결과#

  • Subject와 Observer 클래스 간에는 추상적인 결합도만이 존재한다.

  • 브로드캐스트(broadcast) 방식의 교류를 가능하게 한다.

  • 예측하지 못한 정보를 갱신한다.

from abc import ABC, abstractmethod


class Subject:
    """
    Know its observers. Any number of Observer objects may observe a
    subject.
    Send a notification to its observers when its state changes.
    """

    def __init__(self):
        self._observers = set()
        self._subject_state = None

    def attach(self, observer):
        observer._subject = self
        self._observers.add(observer)

    def detach(self, observer):
        observer._subject = None
        self._observers.discard(observer)

    def _notify(self):
        for observer in self._observers:
            observer.update(self._subject_state)

    @property
    def subject_state(self):
        return self._subject_state

    @subject_state.setter
    def subject_state(self, arg):
        self._subject_state = arg
        self._notify()


class Observer(ABC):
    """
    Define an updating interface for objects that should be notified of
    changes in a subject.
    """

    def __init__(self):
        self._subject = None
        self._observer_state = None

    @abstractmethod
    def update(self, arg):
        pass


class ConcreteObserver(Observer):
    """
    Implement the Observer updating interface to keep its state
    consistent with the subject's.
    Store state that should stay consistent with the subject's.
    """

    def update(self, arg):
        self._observer_state = arg
        # ...


def main():
    subject = Subject()
    concrete_observer = ConcreteObserver()
    subject.attach(concrete_observer)
    subject.subject_state = 123

상태#

의도#

상태(state)는 객체의 내부 상태에 따라 스스로 행동을 변경할 수 있게끔 허가하는 패턴으로, 이렇게 하면 객체는 마치 자신의 클래스를 바꾸는 것처럼 보인다.

활용성#

  • 객체의 행동이 상태에 따라 달라질 수 있고, 객체의 상태에 따라서 런타임에 행동이 바뀌어야 한다.

  • 어떤 연산에 그 객체의 상태에 따라 달라지는 다중 분기 조건 처리가 너무 많이 들어 있을 때

구조#

상태 패턴을 사용하면 내부 상태가 변경될 때 개체가 동작을 변경할 수 있다. 상태 패턴은 자판기에서 볼 수 있습니다. 자동 판매기는 재고, 투입된 금액, 변경 가능 여부, 선택한 항목 등에 따른 상태가 존재한다. 통화가 입금되고 선택이 이루어지면 자동 판매기는 제품을 배달하고 변경 사항이 없는 경우 배달한다. 상품 및 변경, 예치금 부족으로 상품을 배송하지 않거나, 재고 소진으로 상품을 판매하지 않다.

결과#

  • 상태에 따른 행동을 국소화하며, 서로 다른 상태에 대한 행동을 별도의 객체로 관리한다.

  • 상태 전이를 명확하게 만들며, 상태 객체는 공유될 수 없다.

from abc import ABC, abstractmethod


class Context:
    """
    Define the interface of interest to clients.
    Maintain an instance of a ConcreteState subclass that defines the
    current state.
    """

    def __init__(self, state):
        self._state = state

    def request(self):
        self._state.handle()


class State(ABC):
    """
    Define an interface for encapsulating the behavior associated with a
    particular state of the Context.
    """

    @abstractmethod
    def handle(self):
        pass


class ConcreteStateA(State):
    """
    Implement a behavior associated with a state of the Context.
    """

    def handle(self):
        pass


class ConcreteStateB(State):
    """
    Implement a behavior associated with a state of the Context.
    """

    def handle(self):
        pass


def main():
    concrete_state_a = ConcreteStateA()
    context = Context(concrete_state_a)
    context.request()

전략#

의미#

전략(strategy)은 동일 계열의 알고리즘군을 정의하고, 각각의 알고리즘을 캡슐화하며, 이들을 상호 교환이 가능하도록 만드는 패턴이다. 알고리즘을 사용하는 사용자와 상관없이 독립적으로 알고리즘을 다양하게 변경할 수 있게 한다.

활용성#

  • 행동들이 조금씩 다를 뿐 개념적으로 관련된 많은 클래스들이 존재할 때

  • 알고리즘의 변형이 필요할 때. 이를테면, 기억 공간과 처리 속도 간의 절충에 따라 서로 다른 알고리즘을 정의할 수 있을 것이다.

  • 사용자가 몰라야 하는 데이터를 사용하는 알고리즘이 있을 때

  • 하나의 클래스가 많은 행동을 정의하고, 이런 행동들이 그 클래스의 연산 안에서 복잡한 다중 조건문의 모습을 취할 때

구조#

기본 클래스에 인터페이스 세부 정보를 캡슐화하고 파생 클래스에 구현 세부 정보를 확인한다. 그런 다음 클라이언트는 자신을 인터페이스에 연결할 수 있으며 변경과 관련된 격변을 겪지 않아도 되며, 파생 클래스 수가 변경될 때 영향이 없고 파생 클래스 구현이 변경될 때 영향이 없게 만든다.

전략 패턴은 교환 가능하게 사용할 수 있는 일련의 알고리즘을 정의한다. 공항으로 가는 교통수단은 전략의 한 예이며 자가용 운전, 택시 이용, 공항 셔틀, 시내 버스 또는 리무진 서비스와 같은 여러 교통편이 존재한다. 일부 공항의 경우 공항으로 가는 교통 수단으로 지하철과 헬리콥터를 이용할 수도 있다. 이러한 교통 수단 중 하나는 여행자를 공항으로 데려다 줄 것이며 서로 바꿔서 사용할 수도 있다. 여행자는 비용, 편의성 및 시간 간의 균형을 기반으로 전략을 선택해야 한다.

결과#

  • 동일 계열의 관련 알고리즘군이 생긴다.

  • 서브클래싱을 사용하지 않는 대안이다.

  • 조건문을 없앨 수 있고, 구현의 선택이 가능하다.

  • 사용자(프로그램)는 서로 다른 전략을 알아야 한다.

  • Strategy 객체와 Context 객체 사이에 의사소통 오버헤드가 있으며, 객체 수가 증가한다.

from abc import ABC, abstractmethod


class Context:
    """
    Define the interface of interest to clients.
    Maintain a reference to a Strategy object.
    """

    def __init__(self, strategy):
        self._strategy = strategy

    def context_interface(self):
        self._strategy.algorithm_interface()


class Strategy(ABC):
    """
    Declare an interface common to all supported algorithms. Context
    uses this interface to call the algorithm defined by a
    ConcreteStrategy.
    """

    @abstractmethod
    def algorithm_interface(self):
        pass


class ConcreteStrategyA(Strategy):
    """
    Implement the algorithm using the Strategy interface.
    """

    def algorithm_interface(self):
        pass


class ConcreteStrategyB(Strategy):
    """
    Implement the algorithm using the Strategy interface.
    """

    def algorithm_interface(self):
        pass


def main():
    concrete_strategy_a = ConcreteStrategyA()
    context = Context(concrete_strategy_a)
    context.context_interface()

템플릿 메서드#

의도#

템플릿 메서드(template method)는 객체의 연산에는 알고리즘의 뼈대만을 정의하고 각 단계에서 수행할 구체적 처리는 서브클래스 쪽으로 미루는 패턴이다. 알고리즘의 구조 자체는 그대로 놔둔 채 알고리즘 각 단계의 처리를 서브클래스에서 재정의할 수 있게 한다.

두 개의 다른 구성 요소는 상당한 유사성을 가지고 있지만 공통 인터페이스 또는 구현의 재사용을 보여주지 않는다. 두 구성 요소에 공통적인 변경 사항이 필요한 경우 중복 작업을 수행해야 한다. 템플릿 메서드(template method) 패턴은 작업에서 알고리즘의 골격을 정의하고 일부 단계는 클라이언트 하위 클래스로 미루는 것이다. 템플릿 메서드를 사용하면 하위 클래스가 알고리즘의 구조를 변경하지 않고도 알고리즘의 특정 단계를 재정의할 수 있다. 기본 클래스는 알고리즘 ‘자리 표시자’를 선언하고 파생 클래스는 자리 표시자를 구현한다.

활용성#

  • 어떤 한 알고리즘을 이루는 부분 중 변하지 않는 부분을 한 번 정의해 놓고 다양해질 수 있는 부분은 서브클래스에서 정의할 수 있도록 남겨두고자 할 때

  • 서브클래스의 확장을 제어할 수 있다. 템플릿 메서드가 어떤 특정한 시점에 훅(hook) 연산을 호출하도록 정의함으로써, 그 특정 시점에서만 확장되도록 한다.

구조#

템플릿 메서드는 작업에서 알고리즘의 골격을 정의하고 일부 단계를 하위 클래스로 연기한다. 주택 건축업자는 새로운 구획을 개발할 때 템플릿 방법을 사용하며 일반적인 세분화는 각각에 대해 다양한 변형이 가능한 제한된 수의 평면도로 구성된다. 평면도 내에서 기초, 프레임, 배관 및 배선은 각 집에 대해 동일하고 보다 다양한 모델을 생산하기 위해 건설 후반 단계에서 변형이 도입된다.

from abc import ABC, abstractmethod


class AbstractClass(ABC):
    """
    Define abstract primitive operations that concrete subclasses define
    to implement steps of an algorithm.
    Implement a template method defining the skeleton of an algorithm.
    The template method calls primitive operations as well as operations
    defined in AbstractClass or those of other objects.
    """
    def template_method(self):
        self._primitive_operation_1()
        self._primitive_operation_2()

    @abstractmethod
    def _primitive_operation_1(self):
        pass

    @abstractmethod
    def _primitive_operation_2(self):
        pass


class ConcreteClass(AbstractClass):
    """
    Implement the primitive operations to carry out
    subclass-specificsteps of the algorithm.
    """
    def _primitive_operation_1(self):
        pass

    def _primitive_operation_2(self):
        pass


def main():
    concrete_class = ConcreteClass()
    concrete_class.template_method()

방문자#

의도#

방문자(visitor)는 객체 구조를 이루는 원소에 대해 수행할 연산을 표현하는 패턴으로, 연산을 적용할 원소의 클래스를 변경하지 않고도 새로운 연산을 정의할 수 있게 한다. 많은 데이터에 여러 가지 유형의 처리를 수행할 경우 활용한다. 사용 방법은 데이터 구조 내부를 traversal하는 visitor 클래스로 그 클래스에게 데이터의 처리를 맡김, 새로운 처리를 추가할 때는 새로운 visitor를 생성한다. 방문자 패턴을사사용하면 작업하는 요소의 클래스를 변경하지 않고 새 작업을 정의할 수 있으며 손실된 유형 정보를 복구하는 고전적인 기술이다.

활용성#

  • 다른 인터페이스를 가진 클래스가 객체 구조에 포함되어 있으며, 국체 클래스에 따라 달라진 연산을 이들 클래스의 객체에 대해 수행하고자 할 때

  • 객체 구조를 정의한 클래스는 거의 변하지 않지만, 전체 구조에 걸쳐 새로운 연산을 추가하고 싶을 때

구조#

방문자 패턴은 작동하는 클래스를 변경하지 않고 개체 구조의 요소에 대해 수행할 작업을 나타낸다. 방문자 패턴은 택시 회사의 운영에서 볼 수 있다. 사람이 택시회사에 전화를 걸면(손님 접수), 회사에서 손님에게 택시를 파견한다. 택시에 탑승하면 고객 또는 방문자는 더 이상 자신의 운송 수단을 제어할 수 없으며 택시(운전사)가 주체가 된다.

결과#

  • Visitor 클래스는 새로운 연산을 쉽게 추가한다.

  • 방문자를 통해 관련된 연산들을 한 군데로 모으고 관련되지 않은 연산을 떼어낼 수 있다.

  • 새로운 ConcreteElement 클래스를 추가하기 어렵다.

  • 클래스 계층 구조에 걸쳐서 방문한다.

from abc import ABC, abstractmethod


class Element(ABC):
    """
    Define an Accept operation that takes a visitor as an argument.
    """

    @abstractmethod
    def accept(self, visitor):
        pass


class ConcreteElementA(Element):
    """
    Implement an Accept operation that takes a visitor as an argument.
    """

    def accept(self, visitor):
        visitor.visit_concrete_element_a(self)


class ConcreteElementB(Element):
    """
    Implement an Accept operation that takes a visitor as an argument.
    """

    def accept(self, visitor):
        visitor.visit_concrete_element_b(self)


class Visitor(ABC):
    """
    Declare a Visit operation for each class of ConcreteElement in the
    object structure. The operation's name and signature identifies the
    class that sends the Visit request to the visitor. That lets the
    visitor determine the concrete class of the element being visited.
    Then the visitor can access the element directly through its
    particular interface.
    """

    @abstractmethod
    def visit_concrete_element_a(self, concrete_element_a):
        pass

    @abstractmethod
    def visit_concrete_element_b(self, concrete_element_b):
        pass


class ConcreteVisitor1(Visitor):
    """
    Implement each operation declared by Visitor. Each operation
    implements a fragment of the algorithm defined for the corresponding
    class of object in the structure. ConcreteVisitor provides the
    context for the algorithm and stores its local state. This state
    often accumulates results during the traversal of the structure.
    """

    def visit_concrete_element_a(self, concrete_element_a):
        pass

    def visit_concrete_element_b(self, concrete_element_b):
        pass


class ConcreteVisitor2(Visitor):
    """
    Implement each operation declared by Visitor. Each operation
    implements a fragment of the algorithm defined for the corresponding
    class of object in the structure. ConcreteVisitor provides the
    context for the algorithm and stores its local state. This state
    often accumulates results during the traversal of the structure.
    """

    def visit_concrete_element_a(self, concrete_element_a):
        pass

    def visit_concrete_element_b(self, concrete_element_b):
        pass


def main():
    concrete_visitor_1 = ConcreteVisitor1()
    concrete_element_a = ConcreteElementA()
    concrete_element_a.accept(concrete_visitor_1)