Паттер Декоратор - описание (шаблон проектирования)

Название и классификация

Декоратор - паттерн, структурирующий объекты - относится к группе структурных паттернов.

Назначение

Динамически добавляет объекту новые обязанности. Является гибкой альтернативой порождению подклассов с целью расширения функциональности.

Псевдоним

Паттерн также встречается под названием Wrapper (обертка) - то есть тут важно не спутать с Адаптером.

Мотивация

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

Добавить новые обязанности допустимо с помощью наследования. При наследовании классу с рамкой вокруг каждого экземпляра подкласса будет рисоваться рамка. Однако это решение статическое, а значит, недостаточно гибкое.
Клиент не может управлять оформлением компонента рамкой.

Более гибким является другой подход: поместить компонент в другой объект, называемый декоратором, который как раз и добавляет рамку. Декоратор следует интерфейсу декорируемого объекта, поэтому его присутствие прозрачно для
клиентов компонента. Декоратор переадресует запросы внутреннему компоненту, но может выполнять и дополнительные действия (например, рисовать рамку) до или после переадресации.

Поскольку декораторы прозрачны, они могут вкладываться друг в друга, добавляя тем самым любое число новых обязанностей.

Например - как показано на сземе ниже можно добавить полосы прокрутки одним декоратором, а рамку другим:
декторатор паттерн шаблон мотивация

Ниже на диаграмме показано, как композиция объекта TextView с объектами BorderDecorator и ScrollDecorator порождает элемент для ввода текста, окруженный рамкой и снабженный полосой прокрутки:

паттерн декоратор вложенные декораторы

Классы ScrollDecorator и BorderDecorator являются подклассами Decorator - абстрактного класса, который представляет визуальные компоненты, применяемые для оформления других визуальных компонентов.
VisualComponent - это абстрактный класс для представления визуальных объектов. В нем определен интерфейс для рисования и обработки событий. Отметим, что класс Decorator просто переадресует запросы на рисование своему компоненту, а его подклассы могут расширять эту операцию.
паттерн декоратор мотивация пример

Подклассы Decorator могут добавлять любые операции для обеспечения необходимой функциональности. Так, операция ScrollTo объекта ScrollDecorator позволяет другим объектам выполнять прокрутку, если им известно о присутствии объекта ScrollDecorator. Важная особенность этого паттерна состоит в том, что декораторы могут употребляться везде, где возможно появление самого объекта VisualComponent. Поэтому клиент не может отличить декорированный
объект от недекорированного,
а значит, и никоим образом не зависит от наличия или отсутствия оформлений.

Применимость

Используйте паттерн декоратор:

  1. для динамического, прозрачного для клиентов добавления обязанностей объектам;
  2. для реализации обязанностей, которые могут быть сняты с объекта;
  3. когда расширение путем порождения подклассов по каким-то причинам неудобно или невозможно. Иногда приходится реализовывать много независимых расширений, так что порождение подклассов для поддержки всех возможных комбинаций приведет к комбинаторному росту их числа. В других случаях определение класса может быть скрыто или почему-либо еще недоступно, так что породить от него подкласс нельзя.

Структура

Струткуру паттерна декоратор можно представить в виде схемы:
паттерн шаблон декоратор структура

Участники

  1. Component (VisualComponent) – компонент - Класс, задает интерфейс для объектов, на которые могут быть динамически возложены дополнительные обязанности, ровно как задает его и для будущих декораторов.
  2. ConcreteComponent (TextView) - конкретный компонент - Определяет объект, на который возлагаются дополнительные обязанности, вешаются дополнительные возможности.
  3. Decorator – декоратор - Хранит ссылку на объект Component и наследует реализацию его интерфейса по-умалчанию;
  4. ConcreteDecorator (BorderDecorator, ScrollDecorator) - конкретный декоратор - Возлагает дополнительные обязанности на компонент.

Отношения

Клиенты работают "исключительно" с интерфейсом Component. Как видно из структуры паттерна декоратор соответствует этому интерфейсу в силу наследования, а потому клиенту можно передать и декоратор, сконфигурированный особым образом и имеющий ссылку на ConcreteComponent - в этом случае клиент не заметит "подмены", а функционал будет расширен.

Результаты

У шаблона проектирования Декоратор есть как минимум два плюса и два минуса. Поговорим о них подробнее.

  1. Декоратор обеспечивает большую гибкость, чем при статическом наследовании - можно добавлять "только то, что нужно" и даже более того - во время выполнения можно "ослаблять" или "усиливать" функционал объекта - при этом нет необходимости описывать все конкретные комбинации "функционалов" в разных классах путём наследования и уточнения
    - можно применить несколько дектораторов к одному объекту (см. пример)
    - ничто не мешает нам добавить один и тот же функционал дважды (напр. нарисовать двойную рамку вокруг объекта) - в то время как двойное наследование от класса в лучшем случае черевато ошибками =)
  2. Декоратор позволяет избежать перегруженных функциями классов на верхних уровнях иерархии - так как необходимый функционал можно "хранить отдельно [как раз таки в декораторе] и добавлять по мере необходимости"
    - как уже было сказано - нет нужды поддерживать в классе все мыслимые возможности
    - приложение перестаёт "платить" за неиспользуемые функции
    - новые виды декораторов объявляются независимо от расширяемых классов - и помогут нам в даже в том случае, когда подобные расширения не планировались - когда же мы расширяем сложный класс (наследование) нам приходится вникать в детали его реализации
  3. Декоратор и его компонент всё таки не идентичны - следует помнить, что "прозрачность" использования декоратора всё же является условной и зависит от реализации проброса запросов самим декторатором -
    при этом некоторые операции могут быть заменены то есть их реализация в любом случае оказывается различной (даже если возвращаемые значения одинаковы) - об этом не следует забывать, особенно если используете чью-то уже написанную библиотеку
  4. Множество мелких объектов - в каком-то смысле это "естественное" следствие применение декоратора является противовесом ещё большему множеству "крупных" объектов, состоящих в сложной иерархии, но тем не менее - если известно что система оперирует только несколькими классами и резкий рост их числа не планируется, то, возможно, более логичным оказывается всё же сгруппировать функционал в несколько более крупных объектах.

    Если же у нас много мелких классов - то мы сможем собирать много "оптимальных" объектов путём агрегации (напр. "декорирования" по примеру этой статьи), но разобраться в такой системе - как и отлаживать её оказывается более сложным, чем в случае с меньшим числом более крупных классов

Реализация

При реализации паттерна Декторатор следует поразмыслить над следующими моментами:

  1. Соответствие интерфейсов - интерфейсы декторатора и декорируемого (/потенциально декорируемого) им объекта должны совпадать (основания для этого изложены выше).
  2. Отсутствие абстрактного класса Decorator - если планируется добавить только одну обязанность (а в общем случае только один фиксированный их набор) то вам вполне хватит одного конкретного (без абстрактного) класс для декоратора -в этом случае ответственность за переадресацию запросов ложится на ConcreteDecorator
  3. Облегчённые классы Component - класс Component должен быть максимально "лёгким" - просто опредалять интерфейс, а не хранить данные -иначе декораторы могут стать весбма тяжеловесными - и применять их в большом количестве будет накладно - в частности порождения класса, с инициаллизацией множества полей требует ресурса, особенно если эти поля соответствуют типам сложных структур (которые в некоторых язык описываются опять же классами - то есть представляют собой объекты во время исполнения)
    - но и включение большого числа функций в Component увеличивает вероятность того, что декораторам - да и другим подклассам тоже придётся "платить за то, что им не нужно".
  4. Изменение облика, а не внутреннего устройства объекта - следует помнить, что декоратор - не зря периодически называют "оболочкой" (обёрткой) - концепция такова, что изменяется "облик" объекта - но не его внутреннее поведение. То есть функционал добавляется путём помещения исходного компонента в специальную оболочку.

    Альтернативой последнему является изменение внутреннего устройства объекта, изменение, хорошим примером которого может служить паттерн Стратегия. Стратегию лучше использовать в ситуации, когда Component уже достаточно "тяжёл" ,так что применение паттерна декоратор оказывается слишком накладным.

Пример кода

Рассмотрим пример реализации шаблона проектирования Декоратор на языке РHP

Известные применения

В каркасе ET++ декораторы используются для декорирования потоков

Родственные паттерны

Декоратор можно "сравнить" со следующими паттернами:

  1. Паттерн Адаптер в отличии от декоратора, который вовсе не изменяеть интерфейс объекта, придаёт объекту совершенно новый интерфейс.
  2. Паттерн Компоновщик создаёт комбинированные объекты - в этом смысле декоратор можно считать вырожденным случаем составного (комбинированного) объекта - в данном случае имеется только один компонент (декорируемый элемент). Декоратор добавляет новые обязанности (возможности) - агрегирование объектов не является его целью
  3. Паттерн Стратегия изменяет внутреннее устройство объекта, а декоратор его закрывает его "внешней" оболочкой - эти два способа можно использовать как взаимодополняющие.

----------------------
Источники:

  1. silversoft.net/docs/dp/hires/pat4dfso.htm
  2. Паттерн декоратор: codelib.ru/pattern/decorator/java/?addcode#newFormLnk
  3. Обзор паттернов проектирования: citforum.ru/SE/project/pattern/index.shtml#3.1.2