Я родилась в городке, расположенном на западном берегу реки Амур на Дальнем востоке России. Эта область известна своим влажным континентальным климатом, для которого характерны значительные сезонные перепады температур. Просто огромные перепады! Зимы были очень холодными. Иногда температура падала до -40℃. 

Одним из моих самых ярких воспоминаний о русской зиме был процесс одевания перед выходом на улицу. Можно описать его одним словом: слои. А если в нескольких словах, то — много слоев. И числа им было не счесть. Начиналось все с теплых хлопковых колготок, поверх которых надевались шерстяные штаны, а вслед за ними и шерстяные носки. Затем на рубашку натягивался свитер, на руки — шерстяные варежки на длинной резинке, чтобы исключить вероятность потери, а на голову — тонкая вязаная шапочка. После этого шел черед внешнего слоя, состоящего из шубы, меховой шапки (поверх упомянутой выше вязанной шапочки) и валенков (традиционных русских войлочных сапог, а мои были еще и с вышитыми инициалами). На этом процесс не заканчивался. Шуба, шапка и лицо обматывались шерстяным шарфом так, что оставались видны только глаза. Вуаля — вот теперь все! 

В бизнесе этот способ одевания получил название “многослойная одежда”. Броско и наглядно. Я же пошла дальше и назвала его “капуста” (просто включите воображение). Это мой реальный пример шаблона проектирования Декоратор, краткому обзору которого будет посвящена статья. Но предварительно подготовимся к обсуждению этой темы и поговорим об агрегации.

Что мы знаем об агрегации? 

Согласно определению, “агрегация — это объединение нескольких элементов в единое целое”. Одним из его синонимов является слово “куча”, определяемое как “грязная свалка или множество чего-либо”. Вполне приемлемые определения, но есть один озадачивающий меня момент. Если агрегация представляет собой процесс, результатом которого является куча (причем грязная), то почему же эти два слова синонимы? 

Об абстракции в Java в нескольких словах 

В Java процесс агрегации более понятный и означает отношения между классами, представленными односторонней ассоциацией: “покажи мне свою информацию” (но я тебе свою не покажу ?). Куча — это воплощение агрегации. Вещь может существовать без кучи, а вот кучи без вещей… не бывает! 

Знакомьтесь, вот объект Thing (вещь), у которого есть тип, имя и который существует сам по себе. 

@Builder
public class Thing {
   String type;
   String name;
}

А вот класс Heap (куча), у которого есть высота (большая куча/огромная…) и местоположение, но который не может существовать без Things. 

@Data
public class Heap {
   private String height;
   private String location;
   private List<Thing> thing;
}

Только если у вас есть Thing, например Clothes (одежда), вы можете создать из них Heap. Стоит лишь снять с себя слои и разбросать их по полу ?!

public Heap createHeapOfClothes() {
   Heap heap = new Heap();
   heap.setHeight("huge" "огромная");
   heap.setLocation("on the bathroom floor" "на полу в ванной");
   heap.setThing(Arrays.asList(
           Thing.builder().type("clothes").name("t-shirt" "футболка").build(),
           Thing.builder().type("clothes").name("jeans" "джинсы").build(),
           Thing.builder().type("clothes").name("right sock" "правый носок").build(),
           Thing.builder().type("clothes").name("left sock" "левый носок").build()));
   return heap;
}

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

Одна из моих прошлых статей была посвящена такому паттерну проектирования, как Шаблонный метод. В двух словах передам ее суть. В статье был описан способ подготовки сезонных нарядов (Outfit) для бумажной куклы Тани с использованием вышеуказанного метода. Для его реализации был создан абстрактный класс Outfit, который объявлял методы, описывающие шаги для создания повседневного внешнего вида (CreateDailyLook). Некоторые шаги были объявлены как абстрактные (например, describeTop(), describeBottom(), describeFootwear()), другие имели реализацию по умолчанию (drawDescribedOutfit(), cutDrawnOutfit()). Этот класс также определяет текущий шаблонный метод createDailyLook(), который определял последовательность методов-шагов в алгоритме создания повседневного наряда.

public abstract class Outfit {
   public void createDailyLook() {
       describeTop();
       describeBottom();
       describeFootwear();
       drawDescribedOutfit();
       cutDrawnOutfit();
   }
   public abstract void describeTop();
   public abstract void describeBottom();
   public abstract void describeFootwear();
   public void drawDescribedOutfit() { log.info("drawing outfit according to selections")("рисование наряда в соответствии с выбранными вариантами"); }
   public void cutDrawnOutfit() { log.info("cutting drawn outfit")(вырезание нарисованного наряда"); }
}

На данный момент перед нами стоит задача динамически добавить Outerwear (верхняя одежда) в Outfit без изменения определений базового класса. Есть идеи? Даю подсказку — “слои”, что подразумевает использование шаблона Декоратора. 

Шаблон Декоратор 

Википедия предлагает следующее определение данного паттерна: 

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

В точности наш случай! Этот структурный шаблон расширяет поведение объекта, помещая его в обертку. Самое время вспомнить многослойную одежду. Каждый слой — это декоратор, оборачивающий предыдущий. 

Добавим Outerwear в диаграмму данных Outfit. 

И вот что у нас получилось. Левая сторона диаграммы осталась неизменной. Абстрактный класс Outfit включает Шаблонный метод. Конкретные классы Outfit расширяют базовый класс и могут быть инстанцированы. Их экземпляр является исходным Outfit, который впоследствии будет обернут.

Теперь обратим внимание на правую сторону. Outerwear Decorator представляет из себя абстрактный класс и, наряду с Autumn Outfit и Summer Outfit, расширяет базовый класс Outfit. Главные отличия состоят в том, что декоратор агрегирует другие типы компонентов, которые позволят добавлять слои. Подкласс декоратора Coat Decorator присоединяет новый слой Outwear поверх предыдущих слоев Outfit. 

Надеюсь объяснение было понятным, но для большей ясности рассмотрим пример кода. 

Реализация шаблона Декоратор 

В нашем распоряжении уже есть класс Autumn Outfit, реализованный на примере Шаблонного метода. Это наш базовый Outfit. 

public class AutumnOutfit extends Outfit {
   @Override
   public void describeTop() { log.info("white sweater" "белый свитер"); }
   @Override
   public void describeBottom() { log.info("ripped jeans" "рваные джинсы"); }
   @Override
   public void describeFootwear() { log.info("veja sneakers" "кроссовки veja"); }
}

Теперь нам нужно, чтобы Outwear Decorator добавил больше предметов одежды к базовому классу Outfit с применением агрегации. Вам потребуется использовать подкласс абстрактного класса Decorator для добавления к базовому Outfit. Как уже ранее упоминалось, Outwear Decorator — это подкласс Outfit. Следовательно, конкретный декоратор тоже является им.

@RequiredArgsConstructor
public abstract class OuterwearDecorator extends Outfit {
  
   public abstract void describeOuterwear();
  
   @Override
   public void createDailyLook() {
       describeTop();
       describeBottom();
       describeFootwear();
       describeOuterwear();
       drawDescribedOutfit();
       cutDrawnOutfit();
   }
}

Базовый декоратор содержит новое абстрактное поведение describeOuterwear(), которое будет реализовано конкретным декоратором. И он переопределяет метод createDailyLook(). Хотя как-то я упоминала, что подклассы не переопределяют сам шаблонный метод, но этот случай относится к исключениям из правила. Вы можете переопределить данный метод, обернув его в декораторе. 

На следующем этапе происходит реализация абстрактного метода. Создаем конкретный декоратор Coat Decorator.

@RequiredArgsConstructor
public class CoatDecorator extends OuterwearDecorator {

   private final Outfit outfit;

   @Override
   public void describeOuterwear() { log.info("camel coat" "пальто на верблюжьей шерсти");}
   @Override
   public void describeTop() { outfit.describeTop();}
   @Override
   public void describeBottom() { outfit.describeBottom();}
   @Override
   public void describeFootwear() { outfit.describeFootwear(); }
}

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

И наконец, добавляем новый метод к Outfit Service, который создаст повседневный осенний образ Тани, включив в него пальто.  

public void createAutumnOutfitWithCoat() {
   Outfit autumnOutfit = seasonOutfitFactory.createOutfit(Season.AUTUMN);
   autumnOutfit = new CoatDecorator(autumnOutfit);
   autumnOutfit.createDailyLook();
}

Важно отметить, что в стеке базовый класс Outfit должен быть первым. Далее оборачиваем его в Coat Decorator, а затем вызываем метод createDailyLook() для того, чтобы нарисовать (draw) и вырезать (cut) описанный наряд. Абстрактный декоратор просто делегирует создание повседневного облика объекту Outfit, который он агрегирует. Проверяем логи. 

AutumnOutfit - white sweater (белый свитер)
AutumnOutfit - ripped jeans (рваные джинсы)
AutumnOutfit - veja sneakers (кроссовки veja)
CoatDecorator - camel coat (пальто на верблюжьей шерсти)
Outfit - drawing outfit according to selections (рисование наряда в соответствии с выбранными вариантами)
Outfit - cutting drawn outfit (вырезание нарисованного наряда)

Обратите внимание, что шаги Шаблонного метода для создания повседневного внешнего вида были расширены декоратором Coat Decorator без изменения базового класса. Что и требовалось доказать. 

Вот и все! У Тани есть ее бумажная одежда, а я все подумываю о валенках☃️.

Пройдите по ссылке на GitHub для ознакомления с проектом данной статьи. 

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Gene Zeiniss: Design Patterns Saga: The Cabbage

Предыдущая статьяКонтейнеризация в Python. Часть 2
Следующая статьяКонтейнеризация в Python. Часть 3