Что такое паттерн проектирования «Декоратор» (Decorator)?

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

Ответ

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

Зачем он нужен? Когда наследование неудобно или невозможно (например, класс final), или когда нужно добавлять обязанности объекту гибко и комбинировать их в разных порядках.

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

// 1. Базовый компонент
interface Coffee {
    double getCost();
    String getDescription();
}

// 2. Конкретный компонент
class SimpleCoffee implements Coffee {
    @Override
    public double getCost() { return 1.0; }
    @Override
    public String getDescription() { return "Simple coffee"; }
}

// 3. Базовый декоратор (абстрактный)
abstract class CoffeeDecorator implements Coffee {
    protected final Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }
    // Делегирование вызовов обернутому объекту
    @Override
    public double getCost() { return decoratedCoffee.getCost(); }
    @Override
    public String getDescription() { return decoratedCoffee.getDescription(); }
}

// 4. Конкретные декораторы
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) { super(coffee); }

    @Override
    public double getCost() {
        return super.getCost() + 0.5; // Добавляем стоимость молока
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", milk";
    }
}

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) { super(coffee); }

    @Override
    public double getCost() { return super.getCost() + 0.2; }
    @Override
    public String getDescription() { return super.getDescription() + ", sugar"; }
}

// 5. Использование
public class Main {
    public static void main(String[] args) {
        Coffee myCoffee = new SimpleCoffee();
        System.out.println(myCoffee.getDescription() + " $" + myCoffee.getCost());
        // Simple coffee $1.0

        myCoffee = new MilkDecorator(myCoffee); // Декорируем молоком
        myCoffee = new SugarDecorator(myCoffee); // Затем сахаром
        System.out.println(myCoffee.getDescription() + " $" + myCoffee.getCost());
        // Simple coffee, milk, sugar $1.7
    }
}

Ключевые преимущества:

  • Гибкость превыше наследования. Можно добавлять новые обязанности, не создавая иерархии подклассов.
  • Динамическое добавление и удаление. Обёртки можно добавлять и комбинировать во время выполнения.
  • Соблюдение принципа открытости/закрытости.

Классический пример в Java API: потоки ввода-вывода (java.io). Например, BufferedInputStream — это декоратор для FileInputStream, добавляющий буферизацию.

InputStream is = new BufferedInputStream(new FileInputStream("file.txt"));