Decorator design pattern

„Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.“
(Gang Of Four)

Идеята на този design pattern е доста проста – ако искаме да променим какво даден клас прави, да не променяме самият клас или да изполваме наследяване (тоест „compiletime“), а да променяме обектът/обектите му (тоест „runtime“).

Казах „променим“, но по-скоро имах предвид „да допълним“.

Kак?

Като „обвием“ оригиналният („вътрешният“) обект в друг обект („външен“), и когато искаме да извикаме даден метод на „вътрешния“, всъщност вместо това ще извикаме метод на „външният“, който ще извика метод на „вътрешният“.
Отговорът пак ще минава през „външният“ обект.

Отделно външният може да бъде опакован в друг външен и т.н… като лукова глава.

Отделно, може да имаме случаи, в които не искаме да слизаме надолу до Компонента, а направо Декораторът (или някой от Декораторите) да върнат резултат.

Ще наричаме най-вътрешният обект Component, а опаковащият/опаковащите го – Decorator(s).

Важно е да се знае, че всеки „опакован“ обект, трябва да бъде напълно заменяем със своят „оригинал“ (с Component-ът).
„The Decorator is of the same type as the Component“.
„Interface conformance. A decorator object’s interface must conform to the
interface of the component it decorates.“

Този design pattern много напомня на наследяване на класове, но за разлика от наследяването, което се задава предварително, тук можем да постигнем на практика същият ефект динамично (at runtime).
Отделно, ако например нямаме multiple inheritance както в PHP, тук можем да симулираме такова, като използваме в Декоратора различни други класове, за да „украсим“ Компонента.
Да не говорим, че с наследяване може да се наложи според конкретният случай да трябва да пишем отделен клас, което да доведе до „експлозия от класове“.

Да си спомним Single Responsibility Principle, който задава, че един клас трябва да отговаря колкото може по-тясно и конкретно за своята задача, а не да бъде „раздут“ и универсален.
С този design pattern можем динамично да „раздуваме“ обектът Компонент, като използваме други класове, без да нарушаваме този принцип.

Пример:

Ето един прост пример, когато Decorator pattern би бил полезен: имаме пицария, вместо за отделните видове пици да имаме по един отделен клас, можем да имаме един базов клас Pizza който ще е съвсем елементарен, и вече отделни класове Декоратори (имплементиращи общ интерфейс) като Моцарела, КуатроСтажиони и т.н…, на които им се подава обект от Pizza и те го „доокрасяват“.

Идеята е не да има отделен клас за всеки модел пица, а да има отделен клас само за отделните съставки, и отделните пици само да ги комбинират. Това би било много по-гъвкаво, защото какво ако например някоя от съставките поскъпнат? Трябва по всички класове за модели пици да минем и да отразим новата цена.

Другият вариант, който също не е удачен, е да имаме един абсклас Pizza, в който имаме пропъртита за всяка съставка с цената и, и по един метод, от рода на hasOlives(), hasTomato()… на който подаваш true/false и на тази база, цената на съставката се добавя към общата.

Първи недостатък – ако с времето започнем да предлагаме нови и нови съставки, трябва да добавяме за всяка нови и нови пропъртита и has… методи. Което нарушава Open/Close SOLID Principle.

Второ, ами ако клиентът поиска двойни маслини? Трябва hasOlives() например да има още един параметър, или да не подаваме true/false, а quantity и ако е 0 – все едно имаме false.

Трето, нарушаваме и Interface Segregation SOLID Principle, защото щом главният клас Pizza е абстрактен, и has… методите също, трябва във всичките детски класове да ги имплементираме всичките, независимо че може да ни трябват само някои от тях.
„A subclass should not be forced to implement parent methods that it does not use.“

Идеята на Decorator design pattern е да разпредели всички възможни „поведения“ или свойства ако щете, на даден клас, в отделни класове, така че да може като ги имаме отделно, да можем по-гъвкаво да ги комбинираме, вместо твърдо да ги задаваме в всеки клас.

The Decorator pattern has at least two key benefits and two liabilities:

  1. More flexibility than static inheritance. The Decorator pattern provides a more flexible way to add responsibilities to objects than can be had with inheritance. With decorators, responsibilities can be added and removed at run-time simply by attaching and detaching them. In contrast, inheritance requires creating a new class for each additional responsibility.
  2. Avoids feature-laden classes high up in the hierarchy. Decorator offers a pay-as-you-go approach to adding responsibilities. Instead of trying to support all foreseeable features in a complex, customizable class, you can define a simple class and add functionality incrementally with Decorator objects. Functionality can be composed from simple pieces. As a result, an application needn’t pay for
    features it doesn’t use.
  3. A decorator and its component aren’t identical. A decorator acts as a transparent enclosure. But from an object identity point of view, a decorated component is not identical to the component itself. Hence you shouldn’t rely on object identity when you use decorators.
  4. Lots of little objects. A design that uses Decorator often results in systems composed of lots of little objects that all look alike. The objects differ only in the way they are interconnected, not in their class or in the value of their variables. Although these systems are easy to customize by those who understand them, they can be hard to learn and debug.

Литература:

https://en.wikipedia.org/wiki/Decorator_pattern