Что такое паттерн Decorator и как он применяется?

«Что такое паттерн Decorator и как он применяется?» — вопрос из категории Паттерны, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Decorator (Декоратор) — это структурный паттерн проектирования, который позволяет динамически добавлять новую функциональность объектам, оборачивая их в объекты-декораторы, не изменяя их исходный код. Это гибкая альтернатива наследованию для расширения поведения.

Принцип работы:

  1. Создается общий интерфейс для основного объекта и декораторов.
  2. Базовый класс реализует этот интерфейс.
  3. Класс-декоратор также реализует этот интерфейс и содержит ссылку на оборачиваемый объект.
  4. Декоратор делегирует вызовы обернутому объекту, добавляя свою логику до или после.

Пример на Java (Модель кофе с добавками):

// 1. Общий интерфейс
interface Coffee {
    String getDescription();
    double getCost();
}

// 2. Базовый компонент
class SimpleCoffee implements Coffee {
    public String getDescription() { return "Простой кофе"; }
    public double getCost() { return 100.0; }
}

// 3. Базовый абстрактный декоратор (необязательно, но рекомендуется)
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;
    public CoffeeDecorator(Coffee coffee) { this.decoratedCoffee = coffee; }
    public String getDescription() { return decoratedCoffee.getDescription(); }
    public double getCost() { return decoratedCoffee.getCost(); }
}

// 4. Конкретные декораторы
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) { super(coffee); }
    public String getDescription() {
        return decoratedCoffee.getDescription() + ", молоко";
    }
    public double getCost() {
        return decoratedCoffee.getCost() + 30.0;
    }
}

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) { super(coffee); }
    public String getDescription() {
        return decoratedCoffee.getDescription() + ", сахар";
    }
    public double getCost() {
        return decoratedCoffee.getCost() + 5.0;
    }
}

// 5. Использование
public class Main {
    public static void main(String[] args) {
        Coffee myCoffee = new SimpleCoffee();
        myCoffee = new MilkDecorator(myCoffee);
        myCoffee = new SugarDecorator(myCoffee);
        myCoffee = new SugarDecorator(myCoffee); // Двойной сахар

        System.out.println(myCoffee.getDescription()); // "Простой кофе, молоко, сахар, сахар"
        System.out.println("Цена: " + myCoffee.getCost()); // 140.0
    }
}

Преимущества:

  • Гибкость: Позволяет добавлять и комбинировать обязанности во время выполнения.
  • Принцип открытости/закрытости (OCP): Код закрыт для модификации, но открыт для расширения.
  • Избегание «взрывного» роста классов: В отличие от наследования, где для каждой комбинации нужен отдельный подкласс.

Недостатки:

  • Множество мелких классов: Может усложнить структуру программы.
  • Сложность инициализации: Клиентский код должен собирать цепочку декораторов, что может быть неочевидно.
  • Сложность конфигурации: Трудно удалить конкретный декоратор из середины цепочки.

Применение в Java: Классический пример — потоки ввода-вывода (BufferedReader, GZIPInputStream), которые оборачивают другие потоки.