文章来源refactoringguru.cn/design-patt…
PHP 组合模式讲解和代码示例
组合是一种结构型设计模式, 你可以使用它将对象组合成树状结构, 并且能像使用独立对象一样使用它们。
对于绝大多数需要生成树状结构的问题来说, 组合都是非常受欢迎的解决方案。 组合最主要的功能是在整个树状结构上递归调用方法并对结果进行汇总。
复杂度:******
流行度:******
使用实例: 组合模式常在与对象树打交道时使用。 最简单的示例是将该模式应用到 DOM 树的元素上, 使得我们能用相同的方式来与树上的组合和简单元素进行合作。
识别方法: 组合可以通过将同一抽象或接口类型的实例放入树状结构的行为方法来轻松识别。
真实世界示例
组合模式可以精简任何使用树状递归结构的工作。 HTML DOM 树就是这种结构的例子。 例如, 各种输入元素可担当叶节点, 而表单和字段集则可担当组合的角色。
请记住, 你可以用组合模式以对待树结构内部元素的方式来在整个 HTML 树上执行各种行为, 而又无需让代码与 DOM 树的具体类相耦合。 此类行为的示例可以是渲染 DOM 元素、 将其导出为多种格式、 校验其组件等等。
使用组合模式时, 你无需在执行行为前检查元素类型是简单还是复杂。 它会根据元素类型决定是马上执行, 还是将其传递给该元素的所有子元素。
** index.php: 真实世界示例
<?php
namespace RefactoringGuru\Composite\RealWorld;
/**
* The base Component class declares an interface for all concrete components,
* both simple and complex.
*
* In our example, we'll be focusing on the rendering behavior of DOM elements.
*/
abstract class FormElement
{
/**
* We can anticipate that all DOM elements require these 3 fields.
*/
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;
}
/**
* Each concrete DOM element must provide its rendering implementation, but
* we can safely assume that all of them are returning strings.
*/
abstract public function render(): string;
}
/**
* This is a Leaf component. Like all the Leaves, it can't have any children.
*/
class Input extends FormElement
{
private $type;
public function __construct(string $name, string $title, string $type)
{
parent::__construct($name, $title);
$this->type = $type;
}
/**
* Since Leaf components don't have any children that may handle the bulk of
* the work for them, usually it is the Leaves who do the most of the heavy-
* lifting within the Composite pattern.
*/
public function render(): string
{
return "<label for="{$this->name}">{$this->title}</label>\n" .
"<input name="{$this->name}" type="{$this->type}" value="{$this->data}">\n";
}
}
/**
* The base Composite class implements the infrastructure for managing child
* objects, reused by all Concrete Composites.
*/
abstract class FieldComposite extends FormElement
{
/**
* @var FormElement[]
*/
protected $fields = [];
/**
* The methods for adding/removing sub-objects.
*/
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;
});
}
/**
* Whereas a Leaf's method just does the job, the Composite's method almost
* always has to take its sub-objects into account.
*
* In this case, the composite can accept structured data.
*
* @param array $data
*/
public function setData($data): void
{
foreach ($this->fields as $name => $field) {
if (isset($data[$name])) {
$field->setData($data[$name]);
}
}
}
/**
* The same logic applies to the getter. It returns the structured data of
* the composite itself (if any) and all the children data.
*/
public function getData(): array
{
$data = [];
foreach ($this->fields as $name => $field) {
$data[$name] = $field->getData();
}
return $data;
}
/**
* The base implementation of the Composite's rendering simply combines
* results of all children. Concrete Composites will be able to reuse this
* implementation in their real rendering implementations.
*/
public function render(): string
{
$output = "";
foreach ($this->fields as $name => $field) {
$output .= $field->render();
}
return $output;
}
}
/**
* The fieldset element is a Concrete Composite.
*/
class Fieldset extends FieldComposite
{
public function render(): string
{
// Note how the combined rendering result of children is incorporated
// into the fieldset tag.
$output = parent::render();
return "<fieldset><legend>{$this->title}</legend>\n$output</fieldset>\n";
}
}
/**
* And so is the form element.
*/
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";
}
}
/**
* The client code gets a convenient interface for building complex tree
* structures.
*/
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;
}
/**
* The form structure can be filled with data from various sources. The Client
* doesn't have to traverse through all form fields to assign data to various
* fields since the form itself can handle that.
*/
function loadProductData(FormElement $form)
{
$data = [
'name' => 'Apple MacBook',
'description' => 'A decent laptop.',
'photo' => [
'caption' => 'Front photo.',
'image' => 'photo1.png',
],
];
$form->setData($data);
}
/**
* The client code can work with form elements using the abstract interface.
* This way, it doesn't matter whether the client works with a simple component
* or a complex composite tree.
*/
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>