Компоновщик — это структурный паттерн, который позволяет создавать дерево объектов и работать с ним так же, как и с единичным объектом.
Компоновщик давно стал синонимом всех задач, связанных с построением дерева объектов. Все операции компоновщика основаны на рекурсии и «суммировании» результатов на ветвях дерева.
Концептуальный пример
Этот пример показывает структуру паттерна Компоновщик , а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
index.php: Пример структуры паттерна
<?php
namespace RefactoringGuru\Composite\Conceptual;
/**
* Базовый класс Компонент объявляет общие операции как для простых, так и для
* сложных объектов структуры.
*/
abstract class Component
{
/**
* @var Component|null
*/
protected $parent;
/**
* При необходимости базовый Компонент может объявить интерфейс для
* установки и получения родителя компонента в древовидной структуре. Он
* также может предоставить некоторую реализацию по умолчанию для этих
* методов.
*/
public function setParent(?Component $parent)
{
$this->parent = $parent;
}
public function getParent(): Component
{
return $this->parent;
}
/**
* В некоторых случаях целесообразно определить операции управления
* потомками прямо в базовом классе Компонент. Таким образом, вам не нужно
* будет предоставлять конкретные классы компонентов клиентскому коду, даже
* во время сборки дерева объектов. Недостаток такого подхода в том, что эти
* методы будут пустыми для компонентов уровня листа.
*/
public function add(Component $component): void { }
public function remove(Component $component): void { }
/**
* Вы можете предоставить метод, который позволит клиентскому коду понять,
* может ли компонент иметь вложенные объекты.
*/
public function isComposite(): bool
{
return false;
}
/**
* Базовый Компонент может сам реализовать некоторое поведение по умолчанию
* или поручить это конкретным классам, объявив метод, содержащий поведение
* абстрактным.
*/
abstract public function operation(): string;
}
/**
* Класс Лист представляет собой конечные объекты структуры. Лист не может иметь
* вложенных компонентов.
*
* Обычно объекты Листьев выполняют фактическую работу, тогда как объекты
* Контейнера лишь делегируют работу своим подкомпонентам.
*/
class Leaf extends Component
{
public function operation(): string
{
return "Leaf";
}
}
/**
* Класс Контейнер содержит сложные компоненты, которые могут иметь вложенные
* компоненты. Обычно объекты Контейнеры делегируют фактическую работу своим
* детям, а затем «суммируют» результат.
*/
class Composite extends Component
{
/**
* @var \SplObjectStorage
*/
protected $children;
public function __construct()
{
$this->children = new \SplObjectStorage();
}
/**
* Объект контейнера может как добавлять компоненты в свой список вложенных
* компонентов, так и удалять их, как простые, так и сложные.
*/
public function add(Component $component): void
{
$this->children->attach($component);
$component->setParent($this);
}
public function remove(Component $component): void
{
$this->children->detach($component);
$component->setParent(null);
}
public function isComposite(): bool
{
return true;
}
/**
* Контейнер выполняет свою основную логику особым образом. Он проходит
* рекурсивно через всех своих детей, собирая и суммируя их результаты.
* Поскольку потомки контейнера передают эти вызовы своим потомкам и так
* далее, в результате обходится всё дерево объектов.
*/
public function operation(): string
{
$results = [];
foreach ($this->children as $child) {
$results[] = $child->operation();
}
return "Branch(" . implode("+", $results) . ")";
}
}
/**
* Клиентский код работает со всеми компонентами через базовый интерфейс.
*/
function clientCode(Component $component)
{
// ...
echo "RESULT: " . $component->operation();
// ...
}
/**
* Таким образом, клиентский код может поддерживать простые компоненты-листья...
*/
$simple = new Leaf();
echo "Client: I've got a simple component:\n";
clientCode($simple);
echo "\n\n";
/**
* ...а также сложные контейнеры.
*/
$tree = new Composite();
$branch1 = new Composite();
$branch1->add(new Leaf());
$branch1->add(new Leaf());
$branch2 = new Composite();
$branch2->add(new Leaf());
$tree->add($branch1);
$tree->add($branch2);
echo "Client: Now I've got a composite tree:\n";
clientCode($tree);
echo "\n\n";
/**
* Благодаря тому, что операции управления потомками объявлены в базовом классе
* Компонента, клиентский код может работать как с простыми, так и со сложными
* компонентами, вне зависимости от их конкретных классов.
*/
function clientCode2(Component $component1, Component $component2)
{
// ...
if ($component1->isComposite()) {
$component1->add($component2);
}
echo "RESULT: " . $component1->operation();
// ...
}
echo "Client: I don't need to check the components classes even when managing the tree:\n";
clientCode2($tree, $simple);
Output.txt: Результат выполнения
Client: I get a simple component:
RESULT: Leaf
Client: Now I get a composite tree:
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf))
Client: I don't need to check the components classes even when managing the tree::
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf)+Leaf)
Пример из реальной жизни
Паттерн Компоновщик может упростить работу с любыми древовидными рекурсивными структурами. Примером такой структуры является DOM-дерево HTML. Например, в то время как различные входные элементы могут служить листьями, сложные элементы, такие как формы и наборы полей, играют роль контейнеров.
Имея это в виду, вы можете использовать паттерн Компоновщик для применения различных типов поведения ко всему дереву HTML точно так же, как и к его внутренним элементам, не привязывая ваш код к конкретным классам дерева DOM. Примерами такого поведения может быть рендеринг элементов DOM, их экспорт в различные форматы, проверка достоверности их частей и т. д.
С паттерном Компоновщик вам не нужно проверять, является ли тип элемента простым или сложным, перед реализацией поведения. В зависимости от типа элемента, оно либо сразу же выполняется, либо передаётся всем дочерним элементам.
index.php: Пример из реальной жизни
<?php
namespace RefactoringGuru\Composite\RealWorld;
/**
* Базовый класс Компонент объявляет интерфейс для всех конкретных компонентов,
* как простых, так и сложных.
*
* В нашем примере мы сосредоточимся на поведении рендеринга элементов DOM.
*/
abstract class FormElement
{
/**
* Можно предположить, что всем элементам DOM будут нужны эти 3 поля.
*/
protected $name;
protected $title;
protected $data;
public function __construct(string $name, string $title)
{
$this->name = $name;
$this->title = $title;
}
public function getName(): string
{
return $this->name;
}
public function setData($data): void
{
$this->data = $data;
}
public function getData(): array
{
return $this->data;
}
/**
* Каждый конкретный элемент DOM должен предоставить свою реализацию
* рендеринга, но мы можем с уверенностью предположить, что все они будут
* возвращать строки.
*/
abstract public function render(): string;
}
/**
* Это компонент-Лист. Как и все Листья, он не может иметь вложенных
* компонентов.
*/
class Input extends FormElement
{
private $type;
public function __construct(string $name, string $title, string $type)
{
parent::__construct($name, $title);
$this->type = $type;
}
/**
* Поскольку у компонентов-Листьев нет вложенных компонентов, которые могут
* выполнять за них основную часть работы, обычно Листья делают большую
* часть тяжёлой работы внутри паттерна Компоновщик.
*/
public function render(): string
{
return "<label for=\"{$this->name}\">{$this->title}</label>\n" .
"<input name=\"{$this->name}\" type=\"{$this->type}\" value=\"{$this->data}\">\n";
}
}
/**
* Базовый класс Контейнер реализует инфраструктуру для управления дочерними
* объектами, повторно используемую всеми Конкретными Контейнерами.
*/
abstract class FieldComposite extends FormElement
{
/**
* @var FormElement[]
*/
protected $fields = [];
/**
* Методы добавления/удаления подобъектов.
*/
public function add(FormElement $field): void
{
$name = $field->getName();
$this->fields[$name] = $field;
}
public function remove(FormElement $component): void
{
$this->fields = array_filter($this->fields, function ($child) use ($component) {
return $child != $component;
});
}
/**
* В то время как метод Листа просто выполняет эту работу, метод Контейнера
* почти всегда должен учитывать его подобъекты.
*
* В этом случае контейнер может принимать структурированные данные.
*
* @param array $data
*/
public function setData($data): void
{
foreach ($this->fields as $name => $field) {
if (isset($data[$name])) {
$field->setData($data[$name]);
}
}
}
/**
* Та же логика применима и к получателю. Он возвращает структурированные
* данные самого контейнера (если они есть), а также все дочерние данные.
*/
public function getData(): array
{
$data = [];
foreach ($this->fields as $name => $field) {
$data[$name] = $field->getData();
}
return $data;
}
/**
* Базовая реализация рендеринга Контейнера просто объединяет результаты
* всех дочерних элементов. Конкретные Контейнеры смогут повторно
* использовать эту реализацию в своих реальных реализациях рендеринга.
*/
public function render(): string
{
$output = "";
foreach ($this->fields as $name => $field) {
$output .= $field->render();
}
return $output;
}
}
/**
* Элемент fieldset представляет собой Конкретный Контейнер.
*/
class Fieldset extends FieldComposite
{
public function render(): string
{
// Обратите внимание, как комбинированный результат рендеринга потомков
// включается в тег fieldset.
$output = parent::render();
return "<fieldset><legend>{$this->title}</legend>\n$output</fieldset>\n";
}
}
/**
* Так же как и элемент формы.
*/
class Form extends FieldComposite
{
protected $url;
public function __construct(string $name, string $title, string $url)
{
parent::__construct($name, $title);
$this->url = $url;
}
public function render(): string
{
$output = parent::render();
return "<form action=\"{$this->url}\">\n<h3>{$this->title}</h3>\n$output</form>\n";
}
}
/**
* Клиентский код получает удобный интерфейс для построения сложных древовидных
* структур.
*/
function getProductForm(): FormElement
{
$form = new Form('product', "Add product", "/product/add");
$form->add(new Input('name', "Name", 'text'));
$form->add(new Input('description', "Description", 'text'));
$picture = new Fieldset('photo', "Product photo");
$picture->add(new Input('caption', "Caption", 'text'));
$picture->add(new Input('image', "Image", 'file'));
$form->add($picture);
return $form;
}
/**
* Структура формы может быть заполнена данными из разных источников. Клиент не
* должен проходить через все поля формы, чтобы назначить данные различным
* полям, так как форма сама может справиться с этим.
*/
function loadProductData(FormElement $form)
{
$data = [
'name' => 'Apple MacBook',
'description' => 'A decent laptop.',
'photo' => [
'caption' => 'Front photo.',
'image' => 'photo1.png',
],
];
$form->setData($data);
}
/**
* Клиентский код может работать с элементами формы, используя абстрактный
* интерфейс. Таким образом, не имеет значения, работает ли клиент с простым
* компонентом или сложным составным деревом.
*/
function renderProduct(FormElement $form)
{
// ..
echo $form->render();
// ..
}
$form = getProductForm();
loadProductData($form);
renderProduct($form);
Output.txt: Результат выполнения
<form action="/product/add">
<h3>Add product</h3>
<label for="name">Name</label>
<input name="name" type="text" value="Apple MacBook">
<label for="description">Description</label>
<input name="description" type="text" value="A decent laptop.">
<fieldset><legend>Product photo</legend>
<label for="caption">Caption</label>
<input name="caption" type="text" value="Front photo.">
<label for="image">Image</label>
<input name="image" type="file" value="photo1.png">
</fieldset>
</form>