设计模式:灵活编程(组合模式) | BNDong
0%

设计模式:灵活编程(组合模式)

组合可比继承提供更多的灵活性。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,用于返回当前部门的职工数,然后再两个部门类 SalesDepartmentTechnologyDepartment 实现了 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 有两 个方法 addDepartmentemployeesaddDepartment 方法用于接受 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 方法,无论是 SalesDepartmentTechnologyDepartment 还是 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();

可以看到我们只需简单的操作就能计算出总的职工数了,组合模式将计算复杂性完全隐藏了。并且符合组合模式的原则:局部类和组合类具有相同的接口。

优化

我们发现,由于 SalesDepartmentTechnologyDepartment 并不需要 addUnitremoveUnit 方法,但是 Unit 将两个方法定义为抽象方法,则子类必须实现,这就导致了代码冗余。所以,需要解决此问题:

定义默认方法

Unit 中定义默认方法,这样在子类中不必要实现 addUnitremoveUnit 方法。

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 方法是用来区分能否调用 addUnitremoveUnit 方法的,如果 getComposite 返回不是 NULL 就说明方法可调用。

这样看起来结构更清晰了,这种结构还可以再优化,这就需要在项目中慢慢调整了。最切合需求的才是最好的!

总结

组合模式能使原本复杂的计算简单化,在适合的业务需求中使用会带来很多益处:

  • 灵活:因为组合模式中的一切类都共享了同一个父类型,所以可以轻松地在设计中添加新的组合对象或者局部对象,而无需大范围地修改代码。
  • 简单:使用组合结构的客户端代码只需设计简单的接口。客户端代码没有必要区分一个对象是组合对象还是局部对象(除了添加新组件时)。
  • 隐式到达:组合模式中的对象通过树型结构组织。每个组合对象中都保存着对子对象的引用。因此对树中某部分的一个小操作可能会产生很大的影响。
  • 显式到达:树型结构可轻松遍历。可以通过迭代树型结构来获取组合对象和局部对象的信息,或对组合对象和局部对象执行批量处理。

但另一方面,组合模式又依赖于其组成部分的简单性。随着我们引入复杂的规则,代码会变得越来越难以维护。

后记

设计模式真是看似简单,其实复杂的东西,理解实现原理很容易,但是想要活用,就要理解模式的思想了。本文部分摘录自《深入PHP 面向对象、模式与实践》,书很好,我看的倒是挺慢的,有兴趣的也可以读读。最后吐槽下用 Visio 画UML图真不太方便,还是用回Astah Professional吧ಠ╭╮ಠ。感谢阅读,再会!!!

↓赏一个鸡腿... 要不,半个也行↓