组合可比继承提供更多的灵活性。composition provides greater flexibility than inheritance. – 《深入PHP 面向对象、模式与实践》
介绍
组合模式可以很好地聚合和管理许多相似的对象,因而对客户端代码来说,一个独立对象和一个对象集合是没有差别的(部分-整体)。组合模式定义了一个单根继承体系,使具有截然不同职责的集合可以并肩工作。
简单来说,组合模式是运用面向对象的方式来有效的处理树形结构,如下图:
- Component:抽象构件,树形结构的根节点,为组合对象声明接口,也可以为共有接口实现缺省行为。
- Leaf:树形结构的叶节点,该节点没有子节点,实现抽象构件所声明的接口。
- Compsite:树形结构的树枝节点,该节点有子节点,实现抽象构件所声明的接口,存储子部件。
下图为UML类图:
问题
先构建一个虚拟场景:统计公司的职工数量。先定义几个模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| abstract class Department { abstract function employees(); }
class SalesDepartment extends Department { public function employees() { return 200; } }
class TechnologyDepartment extends Department { public function employees() { return 123; } }
|
Department
类定义了一个抽象方法 employees
,用于返回当前部门的职工数,然后再两个部门类 SalesDepartment
、TechnologyDepartment
实现了 employees
方法。现在我们可以定义一个单独的类来合并职工数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Company { private $_departments = array();
public function addDepartment(Department $department) { array_push($this->_departments, $department); }
public function employees() { $tem = 0; foreach ($this->_departments as $department) { $tem += $department->employees(); } return $tem; } }
|
Company
有两 个方法 addDepartment
、 employees
。 addDepartment
方法用于接受 Department
对象并保存到 $_departments
中。 employees
方法通过一个简单的迭代遍历所聚合的 Department
对象并调用 employees
方法,计算出总的职工数。
当前模型对现有需求还是可以很好的满足的,但是如果这个公司又成立了分公司,需要将分公司的职工数也统计进来,并且还能够拆离出来又怎样呢?
我们修改 Company
类,使之可以像添加 Department
对象一样添加 Company
对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Company { private $_company = array(); private $_departments = array();
public function addCompany(Company $company) { array_push($this->_company, $company); }
public function addDepartment(Department $department) { array_push($this->_departments, $department); }
public function employees() { $tem = 0; foreach ($this->_company as $company) { $tem += $company->employees(); } foreach ($this->_departments as $department) { $tem += $department->employees(); } return $tem; } }
|
现在来看这个类还算不太复杂,但是随着需求的增加,这个类所提供的功能也会越来越多。我们回过头来看上面的这几个类,都需要有 employees
方法,无论是 SalesDepartment
、 TechnologyDepartment
还是 Company
它们所提供的功能是相同的,统计职工数。这些相似性给我们带来一个必然的结论:因为容器对象与它们包含的对象共享同一个接口,所以它们应该共享同一个类型家族。
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| abstract class Unit { abstract function addUnit(Unit $unit); abstract function removeUnit(Unit $unit); abstract function employees(); } class Company extends Unit{ private $_units = array();
public function addUnit(Unit $unit) { array_push($this->_units, $unit); }
public function removeUnit(Unit $unit) { $this->_units = array_udiff($this->_units, array($unit), function ($a, $b){ return ($a === $b) ? 0 : 1; }); }
public function employees() { $tem = 0; foreach ($this->_units as $unit) { $tem += $unit->employees(); } return $tem; } }
|
Company
可以保存任何类型的 Unit
对象,包括它自己本身。这样保证了每个对象都支持 Unit
定义的方法。先看下调用:
1 2 3 4 5 6 7 8 9 10 11 12
| $main_army = new Company(); $main_army->addUnit(new SalesDepartment()); $main_army->addUnit(new TechnologyDepartment());
$sub_army = new Company(); $sub_army->addUnit(new SalesDepartment()); $sub_army->addUnit(new SalesDepartment()); $sub_army->addUnit(new SalesDepartment());
$main_army->addUnit($sub_army);
$main_army->employees();
|
可以看到我们只需简单的操作就能计算出总的职工数了,组合模式将计算复杂性完全隐藏了。并且符合组合模式的原则:局部类和组合类具有相同的接口。
优化
我们发现,由于 SalesDepartment
和 TechnologyDepartment
并不需要 addUnit
和 removeUnit
方法,但是 Unit
将两个方法定义为抽象方法,则子类必须实现,这就导致了代码冗余。所以,需要解决此问题:
定义默认方法
Unit
中定义默认方法,这样在子类中不必要实现 addUnit
和 removeUnit
方法。
1 2 3 4 5 6 7 8 9 10 11 12
| abstract class Unit { abstract function employees(); public function addUnit(Unit $unit) { throw new UnitException('error msg'); } public function removeUnit(Unit $unit) { throw new UnitException('error msg'); } }
|
虽然定义了默认方法,非法调用就会抛出异常,但是这么处理未必就是好的,我们仍然不知道调用 Unit
对象的 addUnit
方法是否是安全的。因此,采用这种处理方式就要权衡下了。
优化模式结构
虽然可以将添加和删除方法放到局部类中定义,但是这么处理就需要在每个局部类中分开定义,没有统一约束。所以,将基类 Unit
分解为 CompositeUnit
子类型,然后由局部类来继承。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| abstract class Unit { abstract function employees(); public function getComposite() { return null; } } abstract class CompositeUnit extends Unit { private $_units = array();
public function addUnit(Unit $unit) { if (in_array($unit, $this->_units, true)) { return; } $this->_units[] = $unit; }
public function removeUnit(Unit $unit) { $this->_units = array_udiff($this->_units, array($unit), function ($a, $b){ return ($a === $b) ? 0 : 1; }); }
public function getComposite() { return $this; } }
|
可以发现新增了个 getComposite
方法, getComposite
方法是用来区分能否调用 addUnit
、 removeUnit
方法的,如果 getComposite
返回不是 NULL 就说明方法可调用。
这样看起来结构更清晰了,这种结构还可以再优化,这就需要在项目中慢慢调整了。最切合需求的才是最好的!
总结
组合模式能使原本复杂的计算简单化,在适合的业务需求中使用会带来很多益处:
- 灵活:因为组合模式中的一切类都共享了同一个父类型,所以可以轻松地在设计中添加新的组合对象或者局部对象,而无需大范围地修改代码。
- 简单:使用组合结构的客户端代码只需设计简单的接口。客户端代码没有必要区分一个对象是组合对象还是局部对象(除了添加新组件时)。
- 隐式到达:组合模式中的对象通过树型结构组织。每个组合对象中都保存着对子对象的引用。因此对树中某部分的一个小操作可能会产生很大的影响。
- 显式到达:树型结构可轻松遍历。可以通过迭代树型结构来获取组合对象和局部对象的信息,或对组合对象和局部对象执行批量处理。
但另一方面,组合模式又依赖于其组成部分的简单性。随着我们引入复杂的规则,代码会变得越来越难以维护。
后记
设计模式真是看似简单,其实复杂的东西,理解实现原理很容易,但是想要活用,就要理解模式的思想了。本文部分摘录自《深入PHP 面向对象、模式与实践》,书很好,我看的倒是挺慢的,有兴趣的也可以读读。最后吐槽下用 Visio 画UML图真不太方便,还是用回Astah Professional吧ಠ╭╮ಠ。感谢阅读,再会!!!