Decorator는 structural design pattern입니다. 객체에 동적 기능을 추가하기 위해 구조를 개선하는 패턴입니다. 다양한 확장을 위해 객체를 조합합니다.
다른 프로그램이 중요한 이벤트에 대해 사용자에게 알림을 보내는 Notification library를 개발하고 있다고 상상해 보십시오.
Library의 초기 버전은 몇 개의 필드, 생성자 및 단일 Send
method만 있는 Notifier
class를 기반으로 했습니다. Method는 클라이언트로부터 메시지 인수를 수락하고 생성자를 통해 알리미에게 전달 된 이메일 목록으로 메시지를 보낼 수 있습니다. 클라이언트 역할을 하는 타사 앱은 알림 개체를 한 번 만들고 구성한 다음 중요한 이벤트가 발생할 때마다 사용하도록 되어 있었습니다.
어느 시점에서 라이브러리 사용자는 이메일 알림 이상의 것을 기대한다는 것을 알게 됩니다. 그들 중 많은 사람들이 중요한 문제에 대한 SMS를 받기를 원합니다. 다른 사람들은 Facebook에서 알림을 받기를 원하며 물론 기업 사용자는 Slack 알림을 받고 싶어 합니다.
How hard can that be? Notifier 클래스를 확장하고 추가 일림 메서드를 새 하위 클래스에 넣었습니다. 이제 클라이언트는 원하는 알림 클래스를 인스턴스화 하고 모든 추가 알림에 사용하도록 되어 있습니다.
하지만 누군가 합리적으로 질문했습니다. "여러 알림 유형을 한 번에 사용할 수 없는 이유는 무엇입니까? 집에 불이 났다면 모든 채널을 통해 정보를 받고 싶을 것입니다."
한 클래스 내에 여러 알림 메서드를 결합한 특수 하위 클래스를 만들어 이 문제를 해결하려고 했습니다. 그러나 이 접근 방식은 라이브러리 코드 뿐만 아니라 클라이언트 코드도 엄청나게 부풀릴 것임이 곧 분명해졌습니다.
알림 클래스를 구성하는 다른 방법을 찾아서 알림 클래스의 수가 실수로 기네스 기록을 깨지 않도록 해야 합니다.
클래스 확장은 객체의 동작을 변경해야 할 때 가장 먼저 떠오르는 것입니다. 그러나 상속에는 알아야 할 몇 가지 심각한 주의 사항이 있습니다.
Notifier
클래스 안에 남겨두고 다른 모든 알림 메서드를 decorator로 바꿔 보겠습니다.
옷을 입는 것은 데코레이터를 사용하는 예입니다. 추울 때는 스웨터로 몸을 감 쌉니다. 스웨터를 입고 여전히 추우면 위에 재킷을 입을 수 있습니다. 비가 오면 우비를 입을 수 있습니다. 이러한 모든 의복은 기본 행동을 "확장"하지만 귀하의 일부가 아니며 필요하지 않을 때마다 쉽게 옷을 벗을 수 있습니다.
1. Component는 래퍼와 래핑된 객체 모두에 대한 공통 인터페이스를 선언합니다.
2. Concrete Component는 래핑되는 객체의 클래스입니다. 데코레이터가 변경할 수 있는 기본 동작을 정의합니다.
3. Base Decorator 클래스에는 래핑된 객체를 참조하기 위한 필드가 있습니다. 필드의 유형은 concrete component와 데코레이터를 모두 포함할 수 있도록 component 인터페이스로 선언되어야 합니다. 기본 데코레이터는 모든 작업을 래핑된 객체에 위임합니다.
4. Concrete Decorator는 component에 동적으로 추가할 수 있는 추가 동작을 정의합니다. Concrete Decorator는 기본 데코레이터의 메서드를 재정의하고 부모 메서드를 호출하기 전이나 후에 해당 동작을 실행합니다.
5. Client는 component 인터페이스를 통해 모든 객체와 함께 작동하는 한 여러 계층의 데코레이터로 component를 래핑할 수 있습니다.
이 예에서 Decorator 패턴을 사용하면이 데이터를 실제로 사용하는 코드와는 별도로 민감한 데이터를 압축하고 암호화 할 수 있습니다.
애플리케이션은 데코레이터 쌍으로 데이터 소스 객체를 래핑합니다. 두 래퍼 모두 디스크에서 데이터를 쓰고 읽는 방식을 변경합니다.
// 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()
// ...
개체를 사용하는 코드를 손상시키지 않고 런타임에 개체에 추가 동작을 할당 할 수 있어야 하는 경우 Decorator pattern을 사용합니다.
Decorator를 사용하면 비즈니스 로직을 계층을 구성하고 각 계층에 대한 Decorator를 만들고 런타임에 로직의 다양한 조합으로 객체를 구성 할 수 있습니다. 클라이언트 코드는 이러한 모든 개체가 공통 인터페이스를 따르기 때문에 동일한 방식으로 처리 할 수 있습니다.
상속을 사용하여 개체의 동작을 확장하는 것이 어색하거나 불가능할 때 패턴을 사용합니다.
많은 프로그래밍 언어에는 클래스의 추가 확장을 방지하는 데 사용할 수있는 final
키워드가 있습니다. 최종 클래스의 경우 기존 동작을 재사용하는 유일한 방법은 데코레이터 패턴을 사용하여 클래스를 자체 래퍼로 래핑하는 것입니다.
O 새 하위 클래스를 만들지 않고도 개체의 동작을 확장 할 수 있습니다.
O 런타임에 객체에서 책임을 추가하거나 제거 할 수 있습니다.
O 개체를 여러 데코레이터로 래핑하여 여러 동작을 결합 할 수 있습니다.
O 단일 책임 원칙. 가능한 많은 동작 변형을 구현하는 모 놀리 식 클래스를 여러 개의 작은 클래스로 나눌 수 있습니다.
X 래퍼 스택에서 특정 래퍼를 제거하는 것은 어렵습니다.
X 데코레이터의 동작이 데코레이터 스택의 순서에 의존하지 않는 방식으로 데코레이터를 구현하는 것은 어렵습니다.
X 레이어의 초기 구성 코드는 매우 추하게 보일 수 있습니다.