Lapa Knowledge Base
Структурные паттерны

Decorator

Паттерн 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 — это гибкий способ добавления функциональности объектам!

Copyright © 2026