Компоновщик позволяет упростить и стандартизировать взаимодействие между клиентом и группой объектов, представляющих древовидную структуру "составной объект – его части".
Данный шаблон используется если необходимо:
- представить группу объектов в виде "составной объект – его части";
- (и) чтобы клиенты одинаково обращались как к составным объектам, так и к отдельным частям.
Для того, чтобы понять суть Компоновщика рассмотрим следующий пример: предположим, необходимо вывести на экран карту города или ее область. При этом город можно представить как группу районов, каждый район – как группу кварталов, которые в свою очередь состоят из домов, элементов дорог и т.д.
Неделимые, с точки зрения логики приложения, объекты (дома, деревья, прямой участок дороги, повороты) и будут частями, которые в разных комбинациях могут образовывать различные составные объекты (улицы, кварталы, районы, город).
Пусть все классы, представляющие объекты этой древовидной структуры, поддерживают общий интерфейс. Тогда клиент сможет однообразно обращаться как к отдельному "дому", так и к "городу" в целом. При этом, составные объекты, будут переадресовывать запросы всем своим частям. Например, вызов метода отображения района приведет к отображению входящих в него домов, улиц и т.д.
Данный подход применим в различных областях. Например:
- себестоимость автомобиля можно рассчитать как сумму себестоимостей составляющих его агрегатов и трудозатрат, которые в свою очередь так же могут быть разделены на части;
- структуру сайта можно представить в виде дерева, где частями будут страницы с контентом, составляющие разделы и каталоги;
- при расчете статистики по организации, сотрудники являются "частями" участков, которые входят в отделы, входящие в управления и т. д.
Рассмотрим участников шаблона Компоновщик:
- Компонент (Component / IComponent) – определяет интерфейс, общий для составных объектов и и частей. Может предоставлять реализацию "по-умолчанию" для стандартных методов.
- Составной объект (Composite) – объект, включающий в себя "части".
- Часть или Лист (Leaf) – "неделимые" объекты (название "лист" взято по аналогии с наименованием элемента древовидной структуры).
Стоит еще раз отметить, что с точки зрения клиента все объекты являются экземплярами типа Component. Он не различает (и в большинстве случаев не должен различать) составные объекты и их части.
Результатом использования данного шаблона является:
- упрощение архитектуры клиента, т.к. от него скрыты детали реализации составных объектов и он имеет возможность работать со всеми экземплярами одинаково;
- возможность легко добавлять новые типы как составных объектов, так и их частей. При этом нет необходимости изменять код клиента.
Особенности применения шаблона
При использовании шаблона Компоновщик можно столкнуться с ситуацией, когда у одной части может быть несколько родительских элементов. Например, для карты можно обойтись одним экземпляром "дерево" для всех "кварталов". В этом случае можно дополнительно применить шаблон Приспособленец.
Во многих случаях результат работы метода составного объекта напрямую зависит от порядка вызовов входящих в него частей. В таких случаях необходимо учитывать порядок при разработке методов добавления и удаления частей. Это может быть как сортировка, так и методы для обхода коллекции в правильной последовательности (использование шаблона Итератор).
Самый верхний объект древовидной структуры можно рассматривать как сложный. Поэтому для его создания возможно использование шаблона Строитель. Кроме того, для создания частей может быть применен Фабричный метод.
Проектирование интерфейса
Важным моментом является проектирование интерфейса IComponent. Он должен включать в себя как методы составных объектов, так и их частей. Поэтому важно не дать ему превратиться в "божественный" (т.е. собирающий все методы подряд). Для этого необходимо отметить следующее:
- Методы и свойства для операций с частями не могут поддерживаться самими частями по определению. Добавление их в общий интерфейс только утяжеляет его.
- Часть приложения все равно будет стараться различить типы объектов, как, например, при добавлении новой части. В этом случае нет принципиальной разницы между приведением типа и проверкой результата вызова определённого метода для определения возможностей объекта. Последнее так же раскрывает его тип (составной или часть) и позволяет использовать специфические методы и свойства.
- Оставшаяся часть приложения, использует объекты "вслепую". Именно её и можно считать тем клиентом, на основе требований которого можно разрабатывать IComponent.
В результате, можно выделить общий интерфейс и его уточнение для управления потомками:
- IComponent – содержит только общие методы и свойства, не отражающие специфики различных типов объектов, входящих в реализацию Компоновщика.
- IComposite – является спецификацией составного объекта. В частности, включает хранилище потомков и методы управления ими (обход, добавление, удаление, поиск).
Исходя из этого, возможно до двух базовых классов: Component и Composite.
Теоретически, возможно ситуация, когда потребуется отдельный интерфейс только для частей (ILeaf). Однако, это уже стоит рассматривать как признак необходимости пересмотра схемы применения шаблона, его интерфейсов и входящих в его состав объектов.
Может показаться что такой подход противоречит принципу шаблона. Но это не так. Например, в примере с картой основная задача клиента – отображение. В этом случае метод общего интерфейса Draw() будет обеспечивать однообразное использование объектов. Уточняющие интерфейсы потребуются при создании самой карты. Однако, в этот момент для порождающего метода типы и так будут раскрыты.
Реализация шаблона в общем виде
- определяем необходимый набор методов и свойств, которые составят общий интерфейс IComponent;
- специфику составного объекта описываем в IComposite;
- разрабатываем базовую реализацию Component и ее уточнение Composite;
- в процессе работы приложения клиенту для использования передается общий IComponent.
Пример реализации
Рассмотрим на примере создания меню программы.
Создадим абстракный класс Item, который будет представлять из себя элемент пункта меню.
Создадим конкретный класс ClickableItem
Далее создадим класс DropDownItem которое из себя представляет выпадающий список меню.
Перейдем к применению