
PHP 组合模式讲解和代码示例
组合是一种结构型设计模式, 你可以使用它将对象组合成树状结构, 并且能像使用独立对象一样使用它们。
对于绝大多数需要生成树状结构的问题来说, 组合都是非常受欢迎的解决方案。 组合最主要的功能是在整个树状结构上递归调用方法并对结果进行汇总。
复杂度:
流行度:
使用实例: 组合模式常在与对象树打交道时使用。 最简单的示例是将该模式应用到 DOM 树的元素上, 使得我们能用相同的方式来与树上的组合和简单元素进行合作。
识别方法: 组合可以通过将同一抽象或接口类型的实例放入树状结构的行为方法来轻松识别。
概念示例
本例说明了组合设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
了解该模式的结构后, 你可以更容易地理解下面基于真实世界的 PHP 应用案例。
index.php: 概念示例
<?php namespace RefactoringGuru\Composite\Conceptual; /** * The base Component class declares common operations for both simple and * complex objects of a composition. */ abstract class Component { /** * @var Component|null */ protected $parent; /** * Optionally, the base Component can declare an interface for setting and * accessing a parent of the component in a tree structure. It can also * provide some default implementation for these methods. */ public function setParent(?Component $parent) { $this->parent = $parent; } public function getParent(): Component { return $this->parent; } /** * In some cases, it would be beneficial to define the child-management * operations right in the base Component class. This way, you won't need to * expose any concrete component classes to the client code, even during the * object tree assembly. The downside is that these methods will be empty * for the leaf-level components. */ public function add(Component $component): void { } public function remove(Component $component): void { } /** * You can provide a method that lets the client code figure out whether a * component can bear children. */ public function isComposite(): bool { return false; } /** * The base Component may implement some default behavior or leave it to * concrete classes (by declaring the method containing the behavior as * "abstract"). */ abstract public function operation(): string; } /** * The Leaf class represents the end objects of a composition. A leaf can't have * any children. * * Usually, it's the Leaf objects that do the actual work, whereas Composite * objects only delegate to their sub-components. */ class Leaf extends Component { public function operation(): string { return "Leaf"; } } /** * The Composite class represents the complex components that may have children. * Usually, the Composite objects delegate the actual work to their children and * then "sum-up" the result. */ class Composite extends Component { /** * @var \SplObjectStorage */ protected $children; public function __construct() { $this->children = new \SplObjectStorage(); } /** * A composite object can add or remove other components (both simple or * complex) to or from its child list. */ 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; } /** * The Composite executes its primary logic in a particular way. It * traverses recursively through all its children, collecting and summing * their results. Since the composite's children pass these calls to their * children and so forth, the whole object tree is traversed as a result. */ public function operation(): string { $results = []; foreach ($this->children as $child) { $results[] = $child->operation(); } return "Branch(" . implode("+", $results) . ")"; } } /** * The client code works with all of the components via the base interface. */ function clientCode(Component $component) { // ... echo "RESULT: " . $component->operation(); // ... } /** * This way the client code can support the simple leaf components... */ $simple = new Leaf(); echo "Client: I've got a simple component:\n"; clientCode($simple); echo "\n\n"; /** * ...as well as the complex composites. */ $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"; /** * Thanks to the fact that the child-management operations are declared in the * base Component class, the client code can work with any component, simple or * complex, without depending on their concrete classes. */ 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)
真实世界示例
组合模式可以精简任何使用树状递归结构的工作。 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>