Компоновщик (Composite) — шаблон проектирования
22 января 2020
Шаблон предлагает схему создания составных объектов. Данный подход используется при моделировании иерархий, когда ряд объектов необходимо скомпоновать древовидной структурой в одном объединённом (комплексном) объекте.
Ниже приводится реализация паттерна на языке PHP. Скачать исходники (ZIP, 2 Кб)
Шаблон состоит из следующих элементов:
В литературе, описывающей паттерн, Базовый интерфейс называют Компонентом (Component), Запись называют Листом (Leaf), а Категорию именуют Составным объектом (Composite). Но так как перевод нечётко передаёт суть понятий, поэтому я эти обозначения не использую.
В нашем случае, где мы моделируем коробку с заказом, роль Записей будут выполнять товары заказа, а роль Категорий — коробки и другие упаковочные материалы (пакеты, например), в которые поставщики кладут поставляемую продукцию.
У классов Box (коробки) и Product (товары) есть метод getPrice() для получения стоимости объекта. В случае с товаром метод вернёт стоимость товара, а в случае с коробкой метод вернёт сумму стоимости её содержимого.
В конце скрипта, используя метод getPrice(), подсчитывается и выводится стоимость упакованного заказа, а затем через метод getContentList() выводится структура содержимого заказа.
Результат работы скрипта test.php
Пример задачи: сделать модель коробки, в которую упакован заказ.
Ниже приводится реализация паттерна на языке PHP. Скачать исходники (ZIP, 2 Кб)
Шаблон состоит из следующих элементов:
- Базовый интерфейс — интерфейс или абстрактный класс, который описывает общее поведение для всех объектов, входящих в комплексный объект. Базовый интерфейс используется для выноса общих методов и свойств, которые есть и у Записи, и у Категории;
- Запись — объект, который не может иметь потомков (не может иметь дочерних записей и категорий);
- Категория — объект, который может иметь потомков. У объекта есть свойство-массив (в примере ниже это свойство content), хранящее ссылки на дочерние объекты, а так же есть методы по добавлению и удалению элементов из массива. Именно это свойство обеспечивает реализацию шаблона Компоновщик, компонуя и выстраивая древовидные связи между объектами.
В литературе, описывающей паттерн, Базовый интерфейс называют Компонентом (Component), Запись называют Листом (Leaf), а Категорию именуют Составным объектом (Composite). Но так как перевод нечётко передаёт суть понятий, поэтому я эти обозначения не использую.
В нашем случае, где мы моделируем коробку с заказом, роль Записей будут выполнять товары заказа, а роль Категорий — коробки и другие упаковочные материалы (пакеты, например), в которые поставщики кладут поставляемую продукцию.

box_elements.classes.php
Файл содержит следующие классы:- BoxElement — абстрактный класс и он же Базовый интерфейс, задающий общие свойства и методы для Товаров и Коробок;
- Box — класс коробки заказа. В свойстве content находится массив для хранения дочерних элементов, а именно товаров и других, более мелких, коробок. Также у класса есть метод add($element, $key) для добавления объектов в массив дочерних элементов коробки и метод delete($key) для удаления дочерних элементов.
- Product — класс товара заказа.
У классов Box (коробки) и Product (товары) есть метод getPrice() для получения стоимости объекта. В случае с товаром метод вернёт стоимость товара, а в случае с коробкой метод вернёт сумму стоимости её содержимого.
<?php
/**
* Абстрактный класс общих свойств и методов для Товаров и Коробок
*/
abstract class BoxElement
{
/**
* Название элемента
* @var string
*/
protected $name;
/**
* Возвращает стоимость элемента или его содержимого. Если это товар - вернёт стоимость товара,
* а если это коробка - вернёт сумму товаров, находящихся в коробке
* @return int
*/
abstract public function getPrice();
/**
* Возвращает список содержимого в формате HTML
*/
abstract public function getContentList();
}
/**
* Коробка заказа
*/
class Box extends BoxElement
{
/**
* Массив содержимого коробки (массив дочерних элементов)
* @var string
*/
private $content = array();
/**
* Конструктор коробки
* @param string $name - название коробки
*/
public function __construct($name)
{
$this->name = $name;
}
/**
* Добавляет элемент в содержимое коробки
* @param OrdersBoxElement $element - объект коробки или товара заказа
* @param int|string|bool - ключ или индекс элемента, под котороым он будет добавлен в содержимое коробки.
* Если указать значение false, то элемент будет добавлен в конец массива с идексом по умолчанию.
* Если элемент с указанным ключом уже существует, то новый элемент его перезапишет.
* @return void
*/
public function add($element, $key = false)
{
if ($key !== false) {
$this->content[$key] = $element;
} else {
$this->content[] = $element;
}
}
/**
* Удаляет элемент из содержимого коробки
* @param int|string|bool - ключ или индекс элемента, под котороым он будет добавлен в содержимое коробки.
* @return void
*/
public function delete($key)
{
if (isset($this->content[$key])) {
unset($this->content[$key]);
}
}
/**
* Возвращает стоимость содержимого коробки
* @return int
*/
public function getPrice()
{
$sum = 0;
foreach ($this->content as $element) {
$sum = $sum + $element->getPrice();
}
return $sum;
}
/**
* Возвращает список содержимого в формате HTML
* @return string
*/
public function getContentList()
{
//Получаем содержимое из дочерних элементов
$childContent = '';
foreach ($this->content as $element) {
$childContent .= $element->getContentList();
}
if ($childContent == '') {
$childContent = '<div style="color: #999;"><i>Пусто</i></div>';
}
return '
<div style="margin: 0 0 0 0;">
<div><b>– '.htmlspecialchars($this->name).'</b></div>
<div style="padding: 0 0 0 30px;">'.$childContent.'</b></div>
</div>
';
}
}
/**
* Товар заказа
*/
class Product extends BoxElement
{
/**
* Стоимость товара
* @var int
*/
private $price = 0;
/**
* Конструктор коробки
* @param string $name - название товара
* @param int $price - стоимость товара
*/
public function __construct($name, $price)
{
$this->name = $name;
$this->price = $price;
}
/**
* Возвращает стоимость товара
* @return int
*/
public function getPrice()
{
return $this->price;
}
/**
* Возвращает список содержимого в формате HTML
* @return string
*/
public function getContentList()
{
return '<div>– '.htmlspecialchars($this->name).' <b>('.($this->getPrice()).' руб)</b></div>';
}
}
?>
test.php
Тестовый скрипт, в котором создаются коробки заказа, а затем с помощью метода add($element, $key) в них добавляются товары и другие мелкие коробки (пакеты).В конце скрипта, используя метод getPrice(), подсчитывается и выводится стоимость упакованного заказа, а затем через метод getContentList() выводится структура содержимого заказа.
<?php
//Подключение классов для работы с коробками заказов
require_once(__DIR__.'/box_elements.classes.php');
//Коробка заказа
$boxOrder = new Box('Коробка заказа №333');
$boxOrder->add(new Product('Ключ 10х12', 80));
$boxOrder->add(new Product('Ключ 12х14', 110));
$boxOrder->add(new Product('Ключ 17х19', 180));
//Пакет с товарами №1
$package1 = new Box('Пакет с электроникой');
$package1->add(new Product('Набор выключателей', 200));
$package1->add(new Product('Набор термоусадочных трубок', 720));
$package1->add(new Product('Кабель КГ-ХЛ 3х1,5 (2 метра)', 160));
$boxOrder->add($package1);
//Пакет с товарами №2
$package2 = new Box('Пакет с напильниками');
$package2->add(new Product('Напильник плоский, 200 мм', 155));
$package2->add(new Product('Набор надфилей, 6 штук', 194));
$package2->add(new Product('Набор напильников, 8 штук', 1800));
$boxOrder->add($package2);
//Коробка с подарком от поставщика напильников
$giftBox = new Box('Коробка с подарком');
$giftBox->add(new Product('Пинцет для точных работ', 0));
$giftBox->add(new Product('Открытка с промо-кодом', 0));
$package2->add($giftBox);
//Получаем и выводимо стоимость коробки с заказом
$sumBox = $boxOrder->getPrice();
echo 'Стоимость содержимого коробки с заказом = '.$sumBox.' руб.<br><br>';
//Выводим содержимое коробки с заказом
echo $boxOrder->getContentList();
?>
Результат работы скрипта test.php
Стоимость содержимого коробки с заказом = 3599 руб.
– Коробка заказа №333
– Ключ 10х12 (80 руб)
– Ключ 12х14 (110 руб)
– Ключ 17х19 (180 руб)
– Пакет с электроникой
– Набор выключателей (200 руб)
– Набор термоусадочных трубок (720 руб)
– Кабель КГ-ХЛ 3х1,5 (2 метра) (160 руб)
– Пакет с напильниками
– Напильник плоский, 200 мм (155 руб)
– Набор надфилей, 6 штук (194 руб)
– Набор напильников, 8 штук (1800 руб)
– Коробка с подарком
– Пинцет для точных работ (0 руб)
– Открытка с промо-кодом (0 руб)