Decorator[Design Pattern]

SnowCat·2023년 3월 8일
0

Design Pattern

목록 보기
10/23
post-thumbnail

의도

객체들을 새로운 행동들을 포함한 래퍼 객체들 내에 넣어서 행동들을 연결시키는 디자인 패턴

문제

  • 알림 라이브러리를 만들고 있다 가정해 보자. 다른 프로그램들이 사용자들에게 중요한 이벤트들에 대해 알릴 수 있도록 하는 것이다.
  • 초기 라이브러리 구조는 Notifier을 바탕으로 몇개의 필드와 하나의 생성자, send 메서드만 있는 구조이다
  • 기능을 업데이트하면서 메시지 이외에도 슬랙, 이메일, 페이스북 등을 통해 알림을 받을 수 있게하는 기능들을 추가하고자 한다. 이 때 Notifier를 확장해 원하는 알림 클래스를 추가하고, 추가적인 알림 메서드를 넣어 기능을 확장하고 싶다.
  • 그런데 이 경우 여러 유형의 알림을 추가하고자 하는 순간, 겉잡을수 없이 Notifier 클래스가 증가하게 될 것이다.

해결책

  • 클래스의 상속에서는 2가지를 고려해야 한다.
    • 상속은 정적이며, 런타임에서 기존 객체의 행동을 변경해서는 안 된다.
    • 자식 클래스가 2개 이상의 부모 클래스를 가지도록 해서는 안 된다.
  • 이 문제를 해결하고자 하는 경우 상속 대신 집합관계 또는 합성을 사용해야 함
    집합 관계: 한 객체가 다른 객체에 대한 참조를 갖고 일부 작업을 위임하는 방식
    집합 관계 또는 합성을 통해 객체가 여러 클래스들의 행동을 사용할 수 있고, 여러 객체에 대한 참조들을 가지며 객체들의 모든 종류의 작업을 위임하게 됨
  • 데코레이터 패턴에서는 대상 객체와 연결할 수 있는 래퍼를 통해 클라이언트와 데이터를 주고받을 수 있는 집합관계를 구성하게 됨
  • 앞선 예시에서 각각의 Notifier는 데코레이터가 되고, 이들을 전부 감싸는 Base Decorator를 만들어 실제 클라이언트 코드와 소통하게 됨

구조

// 래퍼, 래핑된 객체들에 대한 공통 인터페이스 선언
interface Component {
    operation(): string;
}

// 래핑되는 객체들의 클래스이며, 기본적인 행동들을 정의함
// 기본 행동은 데코레이터가 변형할 수 있음
class ConcreteComponent implements Component {
    public operation(): string {
        return 'ConcreteComponent';
    }
}

// 데코레이터 클래스
class Decorator implements Component {
  	// 래퍼를 참조하기 위한 필드
    protected component: Component;

    constructor(component: Component) {
        this.component = component;
    }

    // 모든 작업은 래핑된 객체들이 진행하게 됨
    public operation(): string {
        return this.component.operation();
    }
}

// 각 컴포넌트마다 달라지는 행동들은 구상 데코레이터 클래스에 정의함
class ConcreteDecoratorA extends Decorator {
	// 오버라이딩을 통해 재정의
    public operation(): string {
        return `ConcreteDecoratorA(${super.operation()})`;
    }
}


class ConcreteDecoratorB extends Decorator {
    public operation(): string {
      // super.operation()을 통해 상위 객체의 행동을 받아오고, 다른 행동들을 추가함
        return `ConcreteDecoratorB(${super.operation()})`;
    }
}

// 클라이언트 코드에서는 래핑된 컴포넌트를 통해 하위 데코레이터와 작업을 수행 가능함
function clientCode(component: Component) {
    // ...
    console.log(`RESULT: ${component.operation()}`);
    // ...
}


const simple = new ConcreteComponent();
console.log('Client: I\'ve got a simple component:');
clientCode(simple); // RESULT: ConcreteComponent
console.log('');

const decorator1 = new ConcreteDecoratorA(simple);
const decorator2 = new ConcreteDecoratorB(decorator1);
console.log('Client: Now I\'ve got a decorated component:');
clientCode(decorator2); //RESULT: ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent))

적용

  • 객체들을 사용하는 코드를 훼손하지 않으면서 런타임에 추가 행동들을 객체에 할당할 떄 사용
    비즈니스 로직을 계층으로 구성하고, 각 계층에 데코레이터를 생성해 런타임시 로직 조합으로 객체를 구성하도록 함
  • 상속을 사용하는것이 어려울 때 사용

구현방법

  1. 비즈니스 로직이 여러 계층 구조로 감싸진 기본 컴포넌트로 표시될 수 있는지 확인
  2. 기본 컴포넌트, 선택적 계층 양쪽에 공통적인 메서드 확인하고 컴포넌트 인터페이스를 제작
  3. 구상 컴포넌트 클래스를 만들고 기초 행동 정의
  4. 기초 데코레이터 클래스 제작
    래핑된 객체에 대한 참조를 가지는 인터페이스로 선언하고, 모든 작업은 래핑된 객체에 위임하는 방식이 되야함
  5. 클래스들이 컴포넌트 인터페이스를 구현하도록 함
  6. 기초 데코레이터를 확장해 구상 데코레이터 생성

장단점

  • 새 자식 클래스 생성 없이 객체의 행동 확장 가능
  • 런타임에 객체들에서부터 행동을 추가/제거 가능
  • 객체를 여러 데코레이터로 래핑해 여러 행동을 합성 가능
  • 다양한 행동들의 변형을 가지는 작은 클래스들을 분리해 단일 책임 원칙 준수
  • 래퍼들의 스택에서 특정 래퍼를 제거하는것은 어려움
  • 데코레이터의 행동은 데코레이터 스택 내의 순서에 의존하게 되기에 순서 의존성을 없에기 어려움
  • 계층들의 초기 설정 코드가 보기 흉할 수 있음

출처:
https://refactoring.guru/ko/design-patterns/decorator

profile
냐아아아아아아아아앙

0개의 댓글