Алгоритмы

Decorator: динамическое расширение поведения объекта

Введение

Наследование — стандартный способ расширения поведения класса, но он статичен (определяется на этапе компиляции и применяется ко ВСЕМ экземплярам производного класса одинаково) — Decorator решает задачу ДИНАМИЧЕСКОГО, во время выполнения, добавления дополнительного поведения к КОНКРЕТНОМУ объекту, без изменения класса этого объекта и без влияния на другие экземпляры того же класса.

Концепция

Decorator оборачивает объект, реализующий определённый интерфейс, другим объектом, реализующим тот же самый интерфейс и делегирующим вызовы оборачиваемому объекту, добавляя при этом собственную, дополнительную логику до или после делегированного вызова — поскольку и оригинальный объект, и декоратор реализуют один и тот же интерфейс, декораторы можно накладывать друг на друга в произвольной комбинации и порядке, динамически комбинируя нужный набор дополнительного поведения для конкретного, отдельного экземпляра, без необходимости создавать отдельный, статически определённый подкласс для каждой возможной комбинации дополнительных возможностей.

Пример кода

#include <iostream>
#include <memory>
#include <string>

class TextComponent
{
public:
    virtual std::string render() const = 0;
    virtual ~TextComponent() = default;
};

class PlainText : public TextComponent
{
public:
    explicit PlainText(const std::string &text) : m_text(text) {}
    std::string render() const override { return m_text; }

private:
    std::string m_text;
};

// Базовый класс декоратора — хранит ОБОРАЧИВАЕМЫЙ компонент, реализует ТОТ ЖЕ интерфейс
class TextDecorator : public TextComponent
{
public:
    explicit TextDecorator(std::unique_ptr<TextComponent> component) : m_component(std::move(component)) {}

protected:
    std::unique_ptr<TextComponent> m_component;
};

class BoldDecorator : public TextDecorator
{
public:
    using TextDecorator::TextDecorator;
    std::string render() const override { return "<b>" + m_component->render() + "</b>"; } // ДОБАВЛЯЕТ обёртку
};

class ItalicDecorator : public TextDecorator
{
public:
    using TextDecorator::TextDecorator;
    std::string render() const override { return "<i>" + m_component->render() + "</i>"; }
};

int main()
{
    std::unique_ptr<TextComponent> text = std::make_unique<PlainText>("Привет, мир");

    // Динамическое НАКЛАДЫВАНИЕ декораторов — порядок и комбинация определяются ВО ВРЕМЯ ВЫПОЛНЕНИЯ
    text = std::make_unique<BoldDecorator>(std::move(text));
    text = std::make_unique<ItalicDecorator>(std::move(text));

    std::cout << text->render() << std::endl; // "<i><b>Привет, мир</b></i>"
    return 0;
}

Пояснения к коду

TextDecorator, наследующий TextComponent (тот же интерфейс, что и оборачиваемый PlainText) и одновременно хранящий m_component (оборачиваемый компонент) как поле, показывает структуру, делающую декораторы взаимозаменяемыми и комбинируемыми — поскольку BoldDecorator и ItalicDecorator оба реализуют тот же TextComponent::render(), их можно накладывать друг на друга в любом порядке и количестве, как демонстрирует последовательное оборачивание text сначала в BoldDecorator, затем в ItalicDecorator. Итоговый результат "<i><b>Привет, мир</b></i>" явно показывает, что порядок накладывания декораторов влияет на итоговый, вложенный результат (внешний декоратор оборачивает результат внутреннего), что является важной, осознанной частью использования этого паттерна — изменение порядка применения BoldDecorator/ItalicDecorator дало бы иной порядок вложенности тегов в итоговой строке.

Подводные камни

  • Чрезмерное накладывание большого количества декораторов, существенно усложняющее понимание реального, итогового поведения объекта без трассировки через всю цепочку оборачивающих декораторов — каждый дополнительный уровень декорирования добавляет косвенность (виртуальный вызов, делегируемый вглубь цепочки), и для очень длинных, сложных цепочек декораторов понимание итогового поведения требует прослеживания через все уровни, что может стать существенно менее прозрачным по сравнению с более прямым, явным кодом для не самых сложных случаев комбинирования дополнительного поведения.
  • Путаница между Decorator и обычным наследованием для решения задачи, для которой статическое, фиксированное на этапе компиляции расширение поведения через производный класс было бы более простым и достаточным решением — Decorator оправдан именно потребностью в ДИНАМИЧЕСКОЙ, изменяемой во время выполнения, или применяемой выборочно лишь к отдельным конкретным экземплярам комбинации дополнительного поведения, и для случаев, где нужное поведение фиксировано и применимо равномерно ко ВСЕМ экземплярам определённого класса, обычное наследование — более простая, прямая альтернатива.
  • Нарушение согласованности интерфейса декоратора с оборачиваемым компонентом, особенно при добавлении декоратором дополнительных, специфичных только для него методов, отсутствующих в базовом интерфейсе — если код, использующий декорированный объект, должен обращаться к этим дополнительным, специфичным методам конкретного декоратора, это требует явного приведения типа (статья 556 цикла синтаксиса), что частично подрывает саму идею взаимозаменяемости декорированного и недекорированного объекта через единый, общий интерфейс.
  • Игнорирование стоимости дополнительной индирекции (виртуальных вызовов) для очень «горячих», часто вызываемых путей кода (аналогично подводным камням цикла синтаксиса про общую стоимость виртуальных вызовов) — для глубоко вложенных цепочек декораторов, вызываемых с очень высокой частотой в критичном по производительности коде, накопленная стоимость множественных виртуальных вызовов через всю цепочку декораторов может стать заметным фактором, требующим явного измерения и, при необходимости, рассмотрения более простой, не декорированной альтернативы для таких конкретных, измеренных как критичные участков кода.