Паттерн Компоновщик - так же известен как Дерево, Composite.
Суть паттерна
Компоновщик - это шаблон структурного проектирования, который позволяет объединять объекты в древовидные структуры, а затем работать с этими структурами, как если бы они были отдельными объектами.
Проблема
Использование шаблона Composite имеет смысл только тогда, когда базовая модель вашего приложения может быть представлена в виде дерева.
Например, представьте, что у вас есть два типа объектов: конфигурация награды и конфигурация сундука. Сундук может содержать несколько Наград, а также несколько Сундуков меньшего размера. Эти маленькие сундуки также могут содержать некоторые награды или даже сундуки меньшего размера и т. д.
Допустим, вы решили создать систему выдачи наград, в которой используются эти классы. Конфигурация Награды может содержать простые награды без упаковки в сундук, а также сундуки с наградами и другие сундуки. Как бы вы рассчитали все награды основываясь на такой конфигурации?
Вы можете попробовать прямой подход: развернуть все сундуки, просмотреть все награды и затем рассчитать награды по их типам. Это было бы осуществимо в реальном мире; но в программе это не так просто, как запустить цикл. Вы должны заранее знать классы наград и сундуков, которые вы проходите, уровень вложенности сундуков и другие неприятные детали. Все это, делает прямой подход либо слишком неудобным, либо даже невозможным.
Решение
Шаблон Composite предполагает, что вы работаете с наградами и сундуками через общий интерфейс, который объявляет метод получения конкретной награды.
Как будет работать этот метод? Для награды будет просто возвращена ДТО награды. Для сундука метод будет просматривать все элементы, содержащиеся в сундуке, генерировать награду, а затем возвращать общие награды для этого сундука. Если бы один из этих предметов был сундуком меньшего размера, этот сундук также начал бы просматривать свое содержимое и так далее, пока не будут рассчитаны награды для всех внутренних компонентов. Сундук может даже добавить некоторые дополнительные параметры к окончательной награде, например коэффициент увеличения количества награды.
Самым большим преимуществом этого подхода является то, что вам не нужно заботиться о конкретных классах объектов, составляющих дерево. Вам не нужно знать, является ли предмет простой наградой или сложным сундуком. Вы можете получить их все одинаково через общий интерфейс. Когда вы вызываете метод, сами объекты передают запрос вниз по дереву.
Применимость
Используйте шаблон Composite, когда вам нужно реализовать древовидную структуру объекта.
Шаблон Composite предоставляет вам два основных типа элементов, которые имеют общий интерфейс: простые листья и сложные контейнеры. Контейнер может состоять как из листьев, так и из других контейнеров. Это позволяет создавать вложенную рекурсивную структуру объекта, напоминающую дерево.
Используйте шаблон, если вы хотите, чтобы клиентский код одинаково обрабатывал как простые, так и сложные элементы.
Все элементы, определенные шаблоном Composite, имеют общий интерфейс. Используя этот интерфейс, клиенту не нужно беспокоиться о конкретном классе объектов, с которыми он работает.
Как реализовать
Убедитесь, что базовая модель вашего приложения может быть представлена в виде древовидной структуры. Попробуйте разбить его на простые элементы и контейнеры. Помните, что контейнеры должны содержать как простые элементы, так и другие контейнеры.
Объявите интерфейс компонента со списком методов, которые имеют смысл как для простых, так и для сложных компонентов.
Создайте листовой класс для представления простых элементов. Программа может иметь несколько разных конечных классов.
Создайте контейнерный класс, для представления сложных элементов. В этом классе предоставьте поле массива для хранения ссылок на подэлементы. Массив должен иметь возможность хранить как листья, так и контейнеры, поэтому убедитесь, что он объявлен с типом интерфейса компонента. При реализации методов интерфейса компонента помните, что контейнер должен делегировать большую часть работы подэлементам.
Наконец, определите методы добавления и удаления дочерних элементов в контейнере. Имейте в виду, что эти операции могут быть объявлены в интерфейсе компонента. Это нарушит принцип разделения интерфейса, потому что методы будут пустыми в конечном классе. Однако клиент сможет одинаково относиться ко всем элементам даже при составлении дерева.
Напишем простую систему по сбору наград.
Создаем базовый интерфейс RewardItem с методом, который будет возвращать список ДТО наград.
public interface RewardItem {
List<?> rewardFor();
}
Далее определим базовый класс наград BaseReward
public abstract class BaseReward implements RewardItem {
private final String type;
BaseReward(String type){
this.type = type;
}
public abstract List<?> rewardFor();
public String getType() {
return type;
}
}
Для примера, я в конструкторе базового типа буду задавать тип награды. И создам несколько типов наград GoldReward, GemReward и ChestReward.
public class GoldReward extends BaseReward {
GoldReward(){
super("GOLD");
}
@Override
public List<?> rewardFor() {
/*
* We can return list of needed DTO objects
* Now we only print it
*/
System.out.println("rewardFor: " + this.getType());
return Collections.emptyList();
}
}
public class GemReward extends BaseReward {
GemReward(){
super("GEM");
}
@Override
public List<?> rewardFor() {
System.out.println("rewardFor: " + this.getType());
return Collections.emptyList();
}
}
public class ChestReward extends BaseReward {
private List<BaseReward> rewards;
ChestReward(){
super("CHEST");
}
@Override
public List<?> rewardFor() {
System.out.println("rewardFor: " + this.getType());
return rewards.stream().flatMap(reward -> reward.rewardFor().stream()).collect(Collectors.toList());
}
public void setRewards(List<BaseReward> rewards) {
this.rewards = rewards;
}
}
Можно заметить что в ChestReward мы бежим по всем внутренним наградам и собираем их в результирующий список.
Давайте протестируем наши награды, для этого создадим несколько из них и вызовем метод rewardFor.
public class Tester {
public static void main(String[] args) {
GoldReward goldReward = new GoldReward();
GemReward gemReward = new GemReward();
ChestReward chestReward = new ChestReward();
ChestReward smallChestReward = new ChestReward();
smallChestReward.setRewards(List.of(gemReward, goldReward));
chestReward.setRewards(List.of(goldReward, gemReward, smallChestReward));
chestReward.rewardFor();
}
}
На основе этого паттерн можно построить достаточно гибкую систему наград, как это было рассказано в предыдущих статьях: