Decorator

chris·2021년 7월 3일
0

design pattern

목록 보기
8/11

Intent

Decorator는 structural design pattern입니다. 객체에 동적 기능을 추가하기 위해 구조를 개선하는 패턴입니다. 다양한 확장을 위해 객체를 조합합니다.

Problem

다른 프로그램이 중요한 이벤트에 대해 사용자에게 알림을 보내는 Notification library를 개발하고 있다고 상상해 보십시오.
Library의 초기 버전은 몇 개의 필드, 생성자 및 단일 Send method만 있는 Notifier class를 기반으로 했습니다. Method는 클라이언트로부터 메시지 인수를 수락하고 생성자를 통해 알리미에게 전달 된 이메일 목록으로 메시지를 보낼 수 있습니다. 클라이언트 역할을 하는 타사 앱은 알림 개체를 한 번 만들고 구성한 다음 중요한 이벤트가 발생할 때마다 사용하도록 되어 있었습니다.

어느 시점에서 라이브러리 사용자는 이메일 알림 이상의 것을 기대한다는 것을 알게 됩니다. 그들 중 많은 사람들이 중요한 문제에 대한 SMS를 받기를 원합니다. 다른 사람들은 Facebook에서 알림을 받기를 원하며 물론 기업 사용자는 Slack 알림을 받고 싶어 합니다.

How hard can that be? Notifier 클래스를 확장하고 추가 일림 메서드를 새 하위 클래스에 넣었습니다. 이제 클라이언트는 원하는 알림 클래스를 인스턴스화 하고 모든 추가 알림에 사용하도록 되어 있습니다.
하지만 누군가 합리적으로 질문했습니다. "여러 알림 유형을 한 번에 사용할 수 없는 이유는 무엇입니까? 집에 불이 났다면 모든 채널을 통해 정보를 받고 싶을 것입니다."
한 클래스 내에 여러 알림 메서드를 결합한 특수 하위 클래스를 만들어 이 문제를 해결하려고 했습니다. 그러나 이 접근 방식은 라이브러리 코드 뿐만 아니라 클라이언트 코드도 엄청나게 부풀릴 것임이 곧 분명해졌습니다.

알림 클래스를 구성하는 다른 방법을 찾아서 알림 클래스의 수가 실수로 기네스 기록을 깨지 않도록 해야 합니다.

Solution

클래스 확장은 객체의 동작을 변경해야 할 때 가장 먼저 떠오르는 것입니다. 그러나 상속에는 알아야 할 몇 가지 심각한 주의 사항이 있습니다.

  • 상속은 정적입니다. 런타임에 기존 개체의 동작을 변경할 수 없습니다. 전체 오브젝트를 다른 하위 클래스에서 만든 다른 오브젝트로만 바꿀 수 있습니다.
  • 하위 클래스는 하나의 상위 클래스만 가질 수 있습니다. 대부분의 언어에서 상속은 한 클래스가 동시에 여러 클래스의 동작을 상속하도록 허용하지 않습니다.
    이러한 경고를 극복하는 방법 중 하나는 상속 대신 Aggregation 또는 Composition 을 사용하는 것입니다. 두 가지 대안 모두 거의 동일한 방식으로 작동합니다. 하나의 객체는 다른 객체에 대한 참조를 가지고 있고 어떤 작업을 위임하는 반면, 상속을 사용하면 객체 자체가 해당 작업을 수행 할 수 있으며 Super class의 동작을 상속합니다.
    이 새로운 접근 방식을 사용하면 연결된 "helper" 개체를 다른 개체로 쉽게 대체하여 런타임에 컨테이너의 동작을 변경할 수 있습니다. 객체는 다양한 클래스의 동작을 사용하여 여러 객체에 대한 참조를 갖고 모든 종류의 작업을 위임 할 수 있습니다. Aggregation/composition은 Decorator를 포함한 많은 디자인 패턴의 핵심 원칙입니다. On that note, let’s return to the pattern discussion.

    "Wrapper"는 패턴의 주요 아이디어를 명확하게 표현하는 Decorator pattern의 닉네임입니다. Wrapper는 일부 대상 개체와 연결할 수 있는 개체입니다. Wrapper는 대상과 동일한 메서드 집합을 포함하고 수신하는 모든 요청을 위임합니다. 그러나 wrapper는 요청을 대상에 전달하기 전이나 후에 무언가를 수행하여 결과를 변경할 수 있습니다.
    When does a simple wrapper become the real decorator? 앞서 언급 했듯이 wrapper는 wrapping된 개체와 동일한 interface를 구현합니다. 이것이 고객의 관점에서 이러한 개체가 동일한 이유입니다. Wrapper의 참조 필드가 해당 interface를 따르는 모든 개체를 허용하도록 합니다. 이렇게 하면 여러 wrapper의 개체를 덮고 모든 wrapper의 결합 된 동작을 추가 할 수 있습니다.
    알림 예제에서 간단한 이메일 알림 동작을 기본 Notifier 클래스 안에 남겨두고 다른 모든 알림 메서드를 decorator로 바꿔 보겠습니다.

    클라이언트 코드는 기본 알림 객체를 클라이언트의 환경 설정과 일치하는 데코레이터 세트로 래핑해야합니다. 결과 개체는 스택으로 구성됩니다.

    스택의 마지막 데코레이터는 클라이언트가 실제로 작업하는 객체입니다. 모든 데코레이터는 기본 알리미와 동일한 인터페이스를 구현 하므로 나머지 클라이언트 코드는 '순수' 알리미 객체와 함께 작동하는지 데코레이팅된 객체와 함께 작동하는지 상관하지 않습니다.
    메시지 서식 지정 또는 수신자 목록 작성과 같은 다른 동작에도 동일한 접근 방식을 적용 할 수 있습니다. 클라이언트는 다른 사용자와 동일한 인터페이스를 따르는 한 모든 사용자 지정 데코레이터로 개체를 장식 할 수 있습니다.

Real-World Analogy


옷을 입는 것은 데코레이터를 사용하는 예입니다. 추울 때는 스웨터로 몸을 감 쌉니다. 스웨터를 입고 여전히 추우면 위에 재킷을 입을 수 있습니다. 비가 오면 우비를 입을 수 있습니다. 이러한 모든 의복은 기본 행동을 "확장"하지만 귀하의 일부가 아니며 필요하지 않을 때마다 쉽게 옷을 벗을 수 있습니다.

Structure


1. Component는 래퍼와 래핑된 객체 모두에 대한 공통 인터페이스를 선언합니다.

2. Concrete Component는 래핑되는 객체의 클래스입니다. 데코레이터가 변경할 수 있는 기본 동작을 정의합니다.

3. Base Decorator 클래스에는 래핑된 객체를 참조하기 위한 필드가 있습니다. 필드의 유형은 concrete component와 데코레이터를 모두 포함할 수 있도록 component 인터페이스로 선언되어야 합니다. 기본 데코레이터는 모든 작업을 래핑된 객체에 위임합니다.

4. Concrete Decorator는 component에 동적으로 추가할 수 있는 추가 동작을 정의합니다. Concrete Decorator는 기본 데코레이터의 메서드를 재정의하고 부모 메서드를 호출하기 전이나 후에 해당 동작을 실행합니다.

5. Client는 component 인터페이스를 통해 모든 객체와 함께 작동하는 한 여러 계층의 데코레이터로 component를 래핑할 수 있습니다.

Pseudocode

이 예에서 Decorator 패턴을 사용하면이 데이터를 실제로 사용하는 코드와는 별도로 민감한 데이터를 압축하고 암호화 할 수 있습니다.

애플리케이션은 데코레이터 쌍으로 데이터 소스 객체를 래핑합니다. 두 래퍼 모두 디스크에서 데이터를 쓰고 읽는 방식을 변경합니다.

  • 데이터가 written to disk 직전에 데코레이터는 데이터를 암호화하고 압축합니다. 원래 클래스는 변경 사항을 모르고 암호화되고 보호 된 데이터를 파일에 씁니다.
  • 디스크에서 read from disk 직후 동일한 데코레이터를 거쳐 압축을 풀고 디코딩합니다.
    데코레이터와 데이터 소스 클래스는 동일한 인터페이스를 구현하므로 클라이언트 코드에서 모두 상호 교환이 가능합니다.
// The component interface defines operations that can be
// altered by decorators.
interface DataSource is
    method writeData(data)
    method readData():data

// Concrete components provide default implementations for the
// operations. There might be several variations of these
// classes in a program.
class FileDataSource implements DataSource is
    constructor FileDataSource(filename) { ... }

    method writeData(data) is
        // Write data to file.

    method readData():data is
        // Read data from file.

// The base decorator class follows the same interface as the
// other components. The primary purpose of this class is to
// define the wrapping interface for all concrete decorators.
// The default implementation of the wrapping code might include
// a field for storing a wrapped component and the means to
// initialize it.
class DataSourceDecorator implements DataSource is
    protected field wrappee: DataSource

    constructor DataSourceDecorator(source: DataSource) is
        wrappee = source

    // The base decorator simply delegates all work to the
    // wrapped component. Extra behaviors can be added in
    // concrete decorators.
    method writeData(data) is
        wrappee.writeData(data)

    // Concrete decorators may call the parent implementation of
    // the operation instead of calling the wrapped object
    // directly. This approach simplifies extension of decorator
    // classes.
    method readData():data is
        return wrappee.readData()

// Concrete decorators must call methods on the wrapped object,
// but may add something of their own to the result. Decorators
// can execute the added behavior either before or after the
// call to a wrapped object.
class EncryptionDecorator extends DataSourceDecorator is
    method writeData(data) is
        // 1. Encrypt passed data.
        // 2. Pass encrypted data to the wrappee's writeData
        // method.

    method readData():data is
        // 1. Get data from the wrappee's readData method.
        // 2. Try to decrypt it if it's encrypted.
        // 3. Return the result.

// You can wrap objects in several layers of decorators.
class CompressionDecorator extends DataSourceDecorator is
    method writeData(data) is
        // 1. Compress passed data.
        // 2. Pass compressed data to the wrappee's writeData
        // method.

    method readData():data is
        // 1. Get data from the wrappee's readData method.
        // 2. Try to decompress it if it's compressed.
        // 3. Return the result.


// Option 1. A simple example of a decorator assembly.
class Application is
    method dumbUsageExample() is
        source = new FileDataSource("somefile.dat")
        source.writeData(salaryRecords)
        // The target file has been written with plain data.

        source = new CompressionDecorator(source)
        source.writeData(salaryRecords)
        // The target file has been written with compressed
        // data.

        source = new EncryptionDecorator(source)
        // The source variable now contains this:
        // Encryption > Compression > FileDataSource
        source.writeData(salaryRecords)
        // The file has been written with compressed and
        // encrypted data.


// Option 2. Client code that uses an external data source.
// SalaryManager objects neither know nor care about data
// storage specifics. They work with a pre-configured data
// source received from the app configurator.
class SalaryManager is
    field source: DataSource

    constructor SalaryManager(source: DataSource) { ... }

    method load() is
        return source.readData()

    method save() is
        source.writeData(salaryRecords)
    // ...Other useful methods...


// The app can assemble different stacks of decorators at
// runtime, depending on the configuration or environment.
class ApplicationConfigurator is
    method configurationExample() is
        source = new FileDataSource("salary.dat")
        if (enabledEncryption)
            source = new EncryptionDecorator(source)
        if (enabledCompression)
            source = new CompressionDecorator(source)

        logger = new SalaryManager(source)
        salary = logger.load()
    // ...

Applicability

개체를 사용하는 코드를 손상시키지 않고 런타임에 개체에 추가 동작을 할당 할 수 있어야 하는 경우 Decorator pattern을 사용합니다.

Decorator를 사용하면 비즈니스 로직을 계층을 구성하고 각 계층에 대한 Decorator를 만들고 런타임에 로직의 다양한 조합으로 객체를 구성 할 수 있습니다. 클라이언트 코드는 이러한 모든 개체가 공통 인터페이스를 따르기 때문에 동일한 방식으로 처리 할 수 있습니다.

상속을 사용하여 개체의 동작을 확장하는 것이 어색하거나 불가능할 때 패턴을 사용합니다.

많은 프로그래밍 언어에는 클래스의 추가 확장을 방지하는 데 사용할 수있는 final 키워드가 있습니다. 최종 클래스의 경우 기존 동작을 재사용하는 유일한 방법은 데코레이터 패턴을 사용하여 클래스를 자체 래퍼로 래핑하는 것입니다.

How to Implement

  1. 비즈니스 도메인이 여러 선택적 레이어가 있는 기본 구성 요소로 표시 될 수 있는지 확인 하십시오.

  2. 기본 구성 요소와 선택적 레이어 모두에 공통적인 방법을 파악합니다. 구성요소 인터페이스를 만들고 해당 메서드를 선언 하십시오.

  3. 구체적인 구성 요소 클래스를 만들고 그 안에 기본 동작을 정의합니다.

  4. 기본 데코레이터 클래스를 만듭니다. 래핑 된 개체에 대한 참조를 저장하기위한 필드가 있어야합니다. 필드는 데코레이터뿐만 아니라 구체적인 구성 요소에 연결할 수 있도록 구성 요소 인터페이스 유형으로 선언되어야합니다. 기본 데코레이터는 모든 작업을 래핑 된 객체에 위임해야합니다.

  5. 모든 클래스가 컴포넌트 인터페이스를 구현하는지 확인하십시오.

  6. 기본 데코레이터에서 확장하여 콘크리트 데코레이터를 만듭니다. 구체적인 데코레이터는 부모 메서드 (항상 래핑 된 개체에 위임)를 호출하기 전이나 후에 동작을 실행해야합니다.

  7. 클라이언트 코드는 데코레이터를 만들고 클라이언트가 필요로 하는 방식으로 구성해야합니다.

Pros and Cons

O 새 하위 클래스를 만들지 않고도 개체의 동작을 확장 할 수 있습니다.
O 런타임에 객체에서 책임을 추가하거나 제거 할 수 있습니다.
O 개체를 여러 데코레이터로 래핑하여 여러 동작을 결합 할 수 있습니다.
O 단일 책임 원칙. 가능한 많은 동작 변형을 구현하는 모 놀리 식 클래스를 여러 개의 작은 클래스로 나눌 수 있습니다.
X 래퍼 스택에서 특정 래퍼를 제거하는 것은 어렵습니다.
X 데코레이터의 동작이 데코레이터 스택의 순서에 의존하지 않는 방식으로 데코레이터를 구현하는 것은 어렵습니다.
X 레이어의 초기 구성 코드는 매우 추하게 보일 수 있습니다.

profile
software engineer

0개의 댓글