Рассматривается структурный шаблон проектирования Composite (Компоновщик), его применение и пример реализации на PHP.
Содержание
- Применение
- Пример использования шаблона
- Структура шаблона
- Реализация
- Применение шаблона Composite
- Итоги
Применение
Шаблон проектирования Composite (Компоновщик) применяют для работы со сложными иерархическими структурами, с элементами которых возможно взаимодействовать через единый интерфейс. Благодаря этому клиентский код может одинаково взаимодействовать как с отдельным элементом, так и с составным объектом, содержащим другие элементы.
Главная особенность такого подхода заключается в том, что составной объект может содержать любые элементы, в том числе другие составные объекты. Благодаря этому иерархия может иметь произвольную глубину вложенности. При этом клиенту не требуется знать внутреннюю структуру этой иерархии и отдельно обрабатывать разные виды элементов.
Пример использования шаблона
Наглядным примером применения шаблона Composite может служить меню приложения. Отдельный пункт меню может быть самостоятельным элементом, а другой пункт может представлять собой подменю, содержащее другие пункты или подменю.
В результате меню образует иерархическую структуру, в которой отдельные пункты и подменю могут взаимодействовать с клиентом через единый интерфейс. Клиенту при этом не требуется знать, является ли выбранный элемент отдельным пунктом или содержит другие элементы.
Структура шаблона
В основе шаблона Composite лежит общий интерфейс, через который клиент взаимодействует со всеми элементами иерархической структуры.
В нашем примере таким интерфейсом будет ItemRenderer. Он определяет общий для элементов меню метод render().
Шаблон включает два типа элементов:
MenuItem— отдельный пункт меню, не содержащий других элементов;Submenu— составной элемент, содержащий другие элементы меню.
Оба класса реализуют интерфейс ItemRenderer, поэтому клиент может работать с ними одинаковым образом.
Submenu хранит коллекцию элементов типа ItemRenderer. Благодаря этому он может содержать как отдельные пункты MenuItem, так и другие Submenu. В результате из таких объектов можно построить иерархию с произвольной глубиной вложенности.
При выполнении операции render() Submenu последовательно вызывает этот метод у каждого содержащегося в нём элемента. Если таким элементом оказывается другой Submenu, тот в свою очередь обрабатывает свои дочерние элементы.
Таким образом, составной объект и отдельный элемент имеют общий интерфейс, а клиенту не требуется знать, с каким именно объектом он работает.
Упрощённо структура шаблона выглядит так:
ItemRenderer
│
┌────────┐
│ │
MenuItem Submenu
│
│
ItemRenderer[]
│
┌─────┐
│ │
MenuItem Submenu
Фрагмент меню с использованием шаблона Composite
Теперь займемся реализацией вышесказанного.
Реализация
Начнём с интерфейса ItemRenderer, который определяет общий метод отображения элементов меню:
interface ItemRenderer
{
public function render(): string;
}
Теперь создадим класс отдельного пункта меню MenuItem. Он хранит название и адрес ссылки и реализует определённый интерфейсом метод render().
final class MenuItem implements ItemRenderer
{
public function __construct(
private readonly string $name,
private readonly string $url
) {
}
public function render(): string
{
return sprintf(
'<li><a href="%s">%s</a></li>',
htmlspecialchars($this->url, ENT_QUOTES, 'UTF-8'),
htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8')
);
}
}
Далее создадим составной элемент Submenu. В отличие от MenuItem, он хранит дочерние элементы в массиве $items.
Важно, что тип элементов коллекции — ItemRenderer. Поэтому Submenu может содержать как отдельные пункты меню, так и другие подменю.
final class Submenu implements ItemRenderer
{
/**
* @var ItemRenderer[]
*/
private array $items = [];
public function __construct(
private readonly string $name
) {
}
public function add(ItemRenderer $item): void
{
$this->items[] = $item;
}
public function remove(ItemRenderer $item): void
{
foreach ($this->items as $key => $child) {
if ($child === $item) {
unset($this->items[$key]);
return;
}
}
}
public function render(): string
{
$html = '<li>';
$html .= '<span>' . htmlspecialchars($this->name) . '</span>';
$html .= '<ul>';
foreach ($this->items as $item) {
$html .= $item->render();
}
$html .= '</ul>';
$html .= '</li>';
return $html;
}
}
Обратите внимание на реализацию метода render() класса Submenu. Для отображения каждого дочернего элемента он вызывает его собственный метод render(). Если дочерним элементом является MenuItem, будет сформирована ссылка. Если это Submenu, он в свою очередь отобразит содержащиеся в нём элементы.
Таким образом, рекурсивная структура обрабатывается без специального кода для каждого уровня вложенности.
Применение шаблона Composite
Теперь создадим меню приложения. Для начала добавим в него отдельные пункты и подменю:
$menu = new Submenu('Главное меню');
$file = new Submenu('Файл');
$open = new MenuItem('Открыть', '/open');
$save = new MenuItem('Сохранить', '/save');
$file->add($open);
$file->add($save);
$menu->add($file);
$menu->add(new MenuItem('Справка', '/help'));
Получившаяся структура имеет следующий вид:
Главное меню├──Файл│ ├──Открыть│ └──Сохранить└──Справка
Добавим в подменю Файл ещё одно подменю:
$recent = new Submenu('Недавние');
$recent->add(
new MenuItem('Документ 1', '/document/1')
);
$recent->add(
new MenuItem('Документ 2', '/document/2')
);
$file->add($recent);
Теперь структура стала глубже:
Главное меню
├── Файл
│ ├── Открыть
│ ├── Сохранить
│ └── Недавние
│ ├── Документ 1
│ └── Документ 2
└── Справка
При этом клиентский код не изменился: для добавления в Submenu ему не нужно выяснять, является ли добавляемый объект отдельным пунктом или другим подменю.
Наконец, чтобы получить готовое меню, достаточно вызвать render() у корневого объекта:
echo $menu->render();
В этот момент начинается рекурсивная обработка всей структуры. Submenu вызывает render() у своих дочерних элементов. Если среди них находится другой Submenu, тот аналогичным образом обрабатывает собственные элементы.
Таким образом, благодаря шаблону Composite клиенту не требуется самостоятельно обходить иерархию и проверять тип каждого объекта. Он взаимодействует с корневым Submenu через тот же интерфейс ItemRenderer, который используется всеми элементами меню.
Итоги
Основная идея шаблона проектирования Composite (Компоновщик) заключается в том, что все элементы иерархической структуры получают единый интерфейс. Благодаря этому клиент может работать с ними одинаковым образом, не зная внутреннего устройства структуры.
В нашем примере MenuItem и Submenu реализуют интерфейс ItemRenderer. Submenu при этом может содержать как отдельные пункты меню, так и другие подменю. Поэтому структуру можно изменять и расширять, не меняя код, который работает с её элементами.
Главное преимущество шаблона — упрощение клиентского кода при работе с древовидными структурами. Клиенту не требуется самостоятельно обходить дерево и учитывать особенности отдельных типов элементов.
Однако Composite не следует применять к любой иерархии объектов. Шаблон наиболее полезен, когда отдельные и составные элементы действительно имеют общее поведение и клиенту удобно работать с ними через единый интерфейс. Если составляющие иерархии существенно различаются по своему назначению и поведению, попытка объединить их одним интерфейсом может, наоборот, усложнить архитектуру.
На этом пока всё.
Успехов в разработке!
Перейти к списку шаблонов проектирования.