Decorator
Decorator — это структурный паттерн проектирования, который позволяет динамически добавлять объектам новую функциональность, оборачивая их в полезные "обертки".
Проблема
Представьте, что вы работаете над библиотекой уведомлений, которая позволяет другим программам уведомлять своих пользователей о важных событиях.
Первоначальная версия библиотеки основывалась на классе Notifier с единственным методом send, который принимал сообщения от клиентов и отправлял их по электронной почте списку разработчиков. Каждый экземпляр класса Notifier можно было настроить с помощью списка получателей.
В какой-то момент вы поняли, что пользователи библиотеки ожидают не только email-уведомлений. Многие из них хотели бы получать SMS о критических проблемах. Другие хотели бы получать уведомления в Facebook или Slack. Некоторые хотели бы получать все типы уведомлений сразу.
Как бы вы структурировали классы библиотеки, чтобы легко добавлять новые типы уведомлений?
Решение
Паттерн Decorator предлагает поместить логику в специальный объект-обертку, который называется декоратором. Декоратор следует тому же интерфейсу, что и оборачиваемый объект, поэтому для клиента они идентичны.
Структура
Component
└── operation(): string
ConcreteComponent
└── operation(): string
BaseDecorator
├── component: Component
└── operation(): string
ConcreteDecoratorA
├── component: Component
├── operation(): string
└── addedBehavior(): string
ConcreteDecoratorB
├── component: Component
├── operation(): string
└── addedState: string
Пример реализации
JavaScript
// Базовый компонент
class Coffee {
getCost() {
return 10; // Базовая стоимость кофе
}
getDescription() {
return 'Простой кофе';
}
}
// Базовый декоратор
class CoffeeDecorator extends Coffee {
constructor(coffee) {
super();
this.coffee = coffee;
}
getCost() {
return this.coffee.getCost();
}
getDescription() {
return this.coffee.getDescription();
}
}
// Конкретные декораторы
class MilkDecorator extends CoffeeDecorator {
getCost() {
return this.coffee.getCost() + 2;
}
getDescription() {
return this.coffee.getDescription() + ', молоко';
}
}
class SugarDecorator extends CoffeeDecorator {
getCost() {
return this.coffee.getCost() + 1;
}
getDescription() {
return this.coffee.getDescription() + ', сахар';
}
}
class VanillaDecorator extends CoffeeDecorator {
getCost() {
return this.coffee.getCost() + 3;
}
getDescription() {
return this.coffee.getDescription() + ', ваниль';
}
}
class WhippedCreamDecorator extends CoffeeDecorator {
getCost() {
return this.coffee.getCost() + 4;
}
getDescription() {
return this.coffee.getDescription() + ', взбитые сливки';
}
}
// Использование
function createCoffee() {
let coffee = new Coffee();
// Добавляем декораторы
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new VanillaDecorator(coffee);
return coffee;
}
function createFancyCoffee() {
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new WhippedCreamDecorator(coffee);
coffee = new VanillaDecorator(coffee);
return coffee;
}
// Тестируем
const simpleCoffee = createCoffee();
const fancyCoffee = createFancyCoffee();
console.log('=== Простой кофе ===');
console.log(`Описание: ${simpleCoffee.getDescription()}`);
console.log(`Стоимость: ${simpleCoffee.getCost()} руб.`);
console.log('\n=== Фантазийный кофе ===');
console.log(`Описание: ${fancyCoffee.getDescription()}`);
console.log(`Стоимость: ${fancyCoffee.getCost()} руб.`);
TypeScript с интерфейсами
// Интерфейс компонента
interface DataSource {
writeData(data: string): void;
readData(): string;
}
// Конкретный компонент
class FileDataSource implements DataSource {
private filename: string;
private data: string = '';
constructor(filename: string) {
this.filename = filename;
}
writeData(data: string): void {
this.data = data;
console.log(`Запись данных в файл ${this.filename}: ${data}`);
}
readData(): string {
console.log(`Чтение данных из файла ${this.filename}: ${this.data}`);
return this.data;
}
}
// Базовый декоратор
abstract class DataSourceDecorator implements DataSource {
protected wrappee: DataSource;
constructor(source: DataSource) {
this.wrappee = source;
}
writeData(data: string): void {
this.wrappee.writeData(data);
}
readData(): string {
return this.wrappee.readData();
}
}
// Конкретные декораторы
class EncryptionDecorator extends DataSourceDecorator {
writeData(data: string): void {
const encryptedData = this.encrypt(data);
this.wrappee.writeData(encryptedData);
}
readData(): string {
const encryptedData = this.wrappee.readData();
return this.decrypt(encryptedData);
}
private encrypt(data: string): string {
// Простое шифрование (в реальности используйте криптографические библиотеки)
return btoa(data); // Base64 кодирование
}
private decrypt(data: string): string {
return atob(data); // Base64 декодирование
}
}
class CompressionDecorator extends DataSourceDecorator {
writeData(data: string): void {
const compressedData = this.compress(data);
this.wrappee.writeData(compressedData);
}
readData(): string {
const compressedData = this.wrappee.readData();
return this.decompress(compressedData);
}
private compress(data: string): string {
// Простое сжатие (в реальности используйте библиотеки сжатия)
return `[COMPRESSED]${data}`;
}
private decompress(data: string): string {
return data.replace('[COMPRESSED]', '');
}
}
// Использование
const file = new FileDataSource('data.txt');
// Простая запись
console.log('=== Простая запись ===');
file.writeData('Секретные данные');
file.readData();
// Запись с шифрованием
console.log('\n=== Запись с шифрованием ===');
const encryptedFile = new EncryptionDecorator(file);
encryptedFile.writeData('Секретные данные');
encryptedFile.readData();
// Запись с шифрованием и сжатием
console.log('\n=== Запись с шифрованием и сжатием ===');
const secureFile = new CompressionDecorator(new EncryptionDecorator(file));
secureFile.writeData('Секретные данные');
secureFile.readData();
Когда использовать
Используйте Decorator, когда:
- Нужно добавить обязанности к отдельным объектам динамически и прозрачно
- Расширение путем порождения подклассов становится непрактичным
- Нужно добавить функциональность, которую можно отменить
- Хотите избежать создания множества подклассов для каждой комбинации
Преимущества
- Больше гибкости, чем наследование
- Можно добавлять и удалять обязанности во время выполнения
- Можно комбинировать несколько декораторов
- Принцип единственной ответственности: каждый декоратор отвечает за одну функцию
Недостатки
- Много маленьких объектов
- Сложно отладить декорированный код
- Может быть сложно реализовать декораторы, которые не зависят друг от друга
Отличия от других паттернов
- Adapter — изменяет интерфейс объекта
- Bridge — разделяет абстракцию и реализацию
- Composite — группирует объекты в древовидные структуры
Реальный пример
// Декоратор для HTTP запросов
class HTTPRequest {
constructor(url, method = 'GET') {
this.url = url;
this.method = method;
this.headers = {};
this.body = null;
}
async execute() {
console.log(`Выполнение ${this.method} запроса к ${this.url}`);
// Здесь была бы реальная логика HTTP запроса
return { status: 200, data: 'response data' };
}
}
// Базовый декоратор для HTTP запросов
class HTTPRequestDecorator {
constructor(request) {
this.request = request;
}
async execute() {
return this.request.execute();
}
}
// Декоратор для добавления авторизации
class AuthDecorator extends HTTPRequestDecorator {
constructor(request, token) {
super(request);
this.token = token;
}
async execute() {
this.request.headers['Authorization'] = `Bearer ${this.token}`;
console.log('Добавлен токен авторизации');
return super.execute();
}
}
// Декоратор для добавления логирования
class LoggingDecorator extends HTTPRequestDecorator {
async execute() {
console.log(`[LOG] Начало запроса: ${this.request.method} ${this.request.url}`);
const startTime = Date.now();
const result = await super.execute();
const duration = Date.now() - startTime;
console.log(`[LOG] Запрос завершен за ${duration}ms со статусом ${result.status}`);
return result;
}
}
// Декоратор для добавления повторных попыток
class RetryDecorator extends HTTPRequestDecorator {
constructor(request, maxRetries = 3) {
super(request);
this.maxRetries = maxRetries;
}
async execute() {
let lastError;
for (let i = 0; i <= this.maxRetries; i++) {
try {
return await super.execute();
} catch (error) {
lastError = error;
if (i < this.maxRetries) {
console.log(`[RETRY] Попытка ${i + 1} неудачна, повторяем...`);
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
throw lastError;
}
}
// Использование
const request = new HTTPRequest('/api/users', 'GET');
// Добавляем декораторы
const decoratedRequest = new RetryDecorator(
new LoggingDecorator(
new AuthDecorator(request, 'my-secret-token')
)
);
decoratedRequest.execute();
Decorator — это гибкий способ добавления функциональности объектам!