Fork me on GitHub

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

decorator.png

  • Сomponent - абстрактный класс который наследуется ConcreteComponent.
  • ConcreteComponent - объект поведение которого собираемся динамически расширять.
  • Decorator реализует тот же интерфейс или абстрактный класс (в нашем случае абстрактный класс), что и декорируемый компонент).
  • ConcreteDecoratorA - содержит переменную экземпляра в которой хранится декорируемый объект (Component)
  • СoncreteDecoratorB - показано что декораторы могут расширять состояние компонента, если нужно.

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

Теперь попробуем использовать данный паттерн практически, допустим (навеяно примером книги) у нас есть некий набор напитков Чай, Кофе имеющих стоимость, и есть набор добавок к напиткам Молоко, Сахар которые тоже имеют стоимость. Готовый продукт подаваемый клиенту содержит напиток с добавками (или без) и в конечном итоге стоимость равна сумме стоимости всех ингридиентов.

Допустим мы реализовали класс Напитка, который умеет возвращать его стоимость. Если мы реализуем класс добавку как класс - декоратор, мы сможем “обертывать” обьект напитка и добавлять стоимость добавки к стоимости напитка.

decorator_1_layer.png

Схематично это может выглядеть как то так, у нас есть объект Чай который декорирован объектом Добавка Сахар. Т.е. на этапе когда я создаю экземпляр класса добавки - Сахар, экземпляр класса напитка Чай у меня уже есть. Когда я создаю экземпляр декоратора-добавки Сахар я передаю ему уже готовый экземпляр напитка Чай. Цена готового напитка рассчитывается как цена Сахар + Чай, соответственно фактически экземпляр класса Сахар при вызове метода возвращающего цену должен вызвать метод возвращающий цену обернутого объекта (Чай) и потом добавить свою цену и вернуть общую стоимость.

На самом деле вложенность может быть сколь угодно уровневой, допустим мы можем обернуть предыдущий пример еще в один тип добавки Молоко.

decorator_2_layer.png

Тогда общая цена напитка будет состоять из “цена Чая” + “цена Сахара” + “цена Молока”. Т.е. при вызове метода расчета стоимости у экземпляра добавки Молоко ( который обертывает экземпляр Сахар, который в свою очередь содержит обернутый экземпляр Чай), вызовется метод расчета стоимости обернутого обьекта (Сахар), в свою очередь метод расчета стоимости экземпляра Сахар вызовет метод обернутого объекта Чай, добавит свою стоимость (Сахара) и вернет значение экземпляру Молоко. Экземпляр Молока добавит свои стоимость к возвращенному (Чай+Сахар) и вернет общую стоимость напитка. Немного запутанно, но на самом деле не очень сложно.

Такие декорации можно комбинировать, очень гибко без изменения кода. Допустим можно сделать Чай с двойным Молоком, нужно лишь два раза обернуть экземпляр класса Чай декоратором Молоко.

Диаграмма классов нашего примера будет выглядеть так:

decorator_uml.png

  • AbstractBeverage абстрактный класс напитка в котором мы определим методы cost() и getDescription(). cost() это абстрактный метод который должен быть реализован в классах реальных напитков. Метод getDescription() метод который наследуется классами настоящих напитков и можно будет использовать для получения названия напитка.

  • GreenTea, BlackCoffee классы реализующие сущности конкретных напитков Кофе и Чай. Методы cost() в них возвращают конкретную стоимость.

  • AbstractCondiments - абстрактный класс наследованный от AbstractBeverage, в нем мы переопределили getDescription() как абстратный, я хочу чтоб конкретные классы добавок обязаны были иметь свои собственные реализации getDescription(). Метод cost() также должен быть определен в конкретном классе добавки, это поведение определено в родительском классе AbstractBeverage.

  • Sugar, Milk - конкретные классы реализующие AbstractCondiments. В конструктор классов я передаю экземпляр конкретного напитка и запоминаю его в переменной self.decorated. Таким образом в каждом экземпляре добавки я запоминаю обьект который в конечном итоге является производным от AbstractBeverage, так как мы все наследуем от него. self.decorated может содержать как конкретный экземпляр напитка, так и обернутую конструкцию добавка(напиток), при чем глубина вложений не важна, мы оперируем на уровне абстракции у которой в конечном итоге знаем, что можно вызвать cost() и getDescription().

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

Реализация у меня получилась похожая на нечто вот такое:

from abc import ABCMeta, abstractmethod


class AbstractBeverage(metaclass=ABCMeta):

    _description = "Abstract beverage description"

    @abstractmethod
    def cost(self):
        return NotImplementedError

    def getDescription(self):
        return self._description


class BlackCoffee(AbstractBeverage):
    _description = "Black coffee"

    def cost(self):
        return 0.50

class GreenTea(AbstractBeverage):
    _description = "Green Tea"

    def cost(self):
        return 0.15


class AbstractCondiments(AbstractBeverage, metaclass=ABCMeta):
    @abstractmethod
    def getDescription(self):
        return NotImplementedError


class Sugar(AbstractCondiments):
    _description = "Sugar"

    def __init__(self, decorated):
        self.decorated = decorated

    def cost(self):
        return self.decorated.cost() + 0.30

    def getDescription(self):
        return self.decorated.getDescription() + " and " + self._description


class Milk(AbstractCondiments):
    _description = "Milk"

    def __init__(self, decorated):
        self.decorated = decorated

    def cost(self):
        return self.decorated.cost() + 0.25

    def getDescription(self):
        print("Bon appetit !!!")
        return self.decorated.getDescription() + " and " + self._description

Теперь попробуем “приготовить” Чай без добавок:

tea = GreenTea()

tea.cost()
Out[24]: 0.15

tea.getDescription()
Out[25]: 'Green Tea'

А теперь приготовим кофе с молоком и сахаром: - создадим экземпляр BlackCoffee

coffee = BlackCoffee()
  • добавим сахара - создавая экземпляр Sugar - передадим созданный экземпляр BlackCofee и таким образом экземпляр Sugar запомнит экземпляр BlackCofee в переменной self.decorated. Мы таким образом декорируем (обертываем) экземпляр BlackCofee :
coffee = Sugar(coffee)
  • добавим аналогично молоко, теперь coffee это уже не просто чистый кофе а экземпляр Sugar содержащий в себе ссылку на чистый кофе экземпляр BlackCoffee , созданный на первом шаге.
coffee = Milk(coffee)
  • теперь переменная coffee содержит ссылку на конструкцию из чего то, что схематично можно представить как Milk(Sugar(BlackCoffee)) Можно получить стоимость полученного напитка:
coffee.cost()
Out[31]: 1.05
  • и правильно в общем получается если сложить цены ингридиентов:
  • цена кофе = 0,50
  • цена сахара = 0,30
  • цена молока = 0,25
  • 0,50 + 0,30 + 0,25 = 1,05

  • теперь можно обернуть еще раз Milk и увидим что стоимость увеличится:

coffee = Milk(coffee)

coffee.cost()
Out[34]: 1.3

Таким образом можно вкладывать экземпляры и метод cost() будет вызываться по цепочке, в конечном итоге мы получим общую стоимость.

    def cost(self):
        return self.decorated.cost() + 0.25

Это происходит потому что мы вызываем метод cost() у чего то обернутого self.decorated и в свою очередь если у “чего то обернутого” в свою очередь содержится обернутый объект у этого объекта также вызовется метод cost(). Так будет происходить пока не дойдет очередь до обьекта у которого нет “чего то обернутого” и у него вызовется метод cost(), он вернет значение вызвавшему его экземпляру, тот добавит свою стоимость и вернет выше, т.е цепочка пойдет в обратном направлении.

(Milk cost()) -> Sugar(cost()) -> BlackCoffee(cost())
(Milk (return 0.80+0.25) <- Sugar(return 0.50+0.30) <- BlackCoffee(return 0.50))

Вывод

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

Следует предесмотреть возможность расширения без изменения существующего кода.

Композиция и делегирование часто используются для динамического добавления нового поведения (в нашем случае мы делегируем вызов cost() обьекту decorated).

Типы декораторов соответствуют типам декорируемых компонентов (соответстве может быть достигнуто посредством наследования или реализации интерфейса)

Компонент может декорироваться любым количеством декораторов.

Декораторы изменяют поведение декорируемых компонентов, добавляя новую функциональность до или после или даже вместо вызовов методов компонентов.


Comments

comments powered by Disqus