【简介】组合比继承具有更大的灵活性。
组合模式:将一组对象组合为可像单个对象一样被使用的结构。
装饰模式:通过在运行时合并对象来扩展功能呢的一种灵活机制。
外观模式:为复杂或多变的系统创建一个简单的接口。
10.1 组合模式
组合模式可以很好地聚合和管理许多相似的对象,有助于我们为集合和组件之间的关系建立模型。
10.1.1 问题
管理一组对象是很复杂的,当对象中可能还包含着它们自己的对象时尤其如此。
我们以一款「文明」的游戏为基础设计一个系统,玩家可以在一个由大量区块所组成的地图上移动战斗单元。独立的单元可被组合起来一起移动、战斗和防守。我们先来定义战斗单元的类(Unit类)。
/* 战斗单元的抽象类
有一个抽象方法bombardStrength()方法用于设置战斗单元的工具强度
*/
abstract class Unit {
abstract function bombardStrength();
}
/* ArcherUnit(射手)类,以及该类的攻击强度 */
class ArcherUnit extends Unit {
function bombardStrength() {
return 1;
}
}
/* LaserCannonUnit(激光炮)类,以及该类的攻击强度 */
class LaserCannonUnit extends Unit {
function bombardStrength() {
return 2;
}
}
Unit 类定义了一个抽象方法 bombardStrength(),用于设置战斗单元对邻近区块的攻击强度。然后在 ArcherUnit(射手)和LaserCannonUnit(激光炮)类都继承了 Unit 类,并各自实现了 bombardStrength() 方法用于定义自己的攻击强度。
现在我们可以定义一个独立的类来组合战斗单元(Army类)。
/* 战斗集合的类 */
class Army {
private $units = array();
/* 用于接收Unit对象,并将对象保存在$units数组中 */
function addUnit(Unit $unit) {
array_push($this->units, $unit);
}
/* 计算出总的工具强度 */
function bombardStrength() {
$ret = 0;
foreach ($this->units as $unit) {
$ret += $unit->bombardStrength();
}
return $ret;
}
}
Army(军队)类中定义一个 addUnit() 方法用于接收 Unit 对象。Unit 对象被保存在 $units 数组中。同时通过bombardStrength() 方法计算出总的攻击强度。
如果 Army(军队)类需要和其他Army类进行合并,同时,每个 Army 都有自己的 ID,这样 Army 在以后还可以从整编中解散出来。我们修改原来的 Army(军队)类,使其同时支持 Unit(单元)和 Army(军队)。
/* 战斗集合的类 */
class Army {
private $units = array();
private $armies = array();
/* 用于接收Unit对象,并将对象保存在$units数组中 */
function addUnit(Unit $unit) {
array_push($this->units, $unit);
}
/* 用于接收Army对象,并将对象保存在$armies数组中 */
function addArmy(Army $army) {
array_push($this->armies, $army);
}
/* 计算出总的工具强度 */
function bombardStrength() {
$ret = 0;
foreach ($this->units as $unit) {
$ret += $unit->bombardStrength();
}
foreach ($this->armies as $army) {
$ret += $army->bombardStrength();
}
return $ret;
}
}
完成了增加 Army(军队)的功能后,后续可以对defensiveStength() 和 movementRange() 等方法做和 bombardStrength() 相似的处理,这个游戏就会变得越来越完善。
但现在有一个新的需求,需要支持运兵船可以支持最多10个战斗单元以改进它们在某些地形上的活动范围。那么按照我们之前 Army(军队)类的处理方式,也可以创建一个 TroopCarrier 类来处理运兵船的需求,因为这也是一个战斗单元组合的需求。
我们发现,无论是 Unit 类,还是 Army 类或者TroopCarrier 类,所需要的功能是一样的:它们都要移动、攻击和防守,也要提供添加和移除其他对象的功能(方法)。这些相似性会给我们一个启发:这些容器对象(Unit、Army、TroopCarrier )与它们包含的对象(ArcherUnit、LaserCannonUnit)共享一个接口(该接口中有移动、攻击bombardStrength和防御,还有添加和移除其他对象的功能)。
10.1.2 实现
组合模式定义了一个继承体系,使具有不同职责的集合可以并肩工作。组合模式中的类必须支持一个共同的操作集。我们来看「文明」游戏问题的组合模式。
模型中所有的类都扩展自 Unit 类,同时,Army 和 TroopCarrier 类被设计成了组合对象,用于包含 Unit 对象。
但是,我们也发现 ArcherUnit 和 LaserCannonUnit 类(称为局部对象,也称为树叶对象,因为组合模式为树形结构,组合对象为树干,单独存在的对象为树叶,树叶对象应为最小单元,其中不能包含其他对象。)不能包含其他 Unit 对象,但也实现了addUnit() 方法,这个问题我们后面讨论。
下面我们重写 Unit 抽象类:
abstract class Unit {
abstract function addUnit(Unit $unit);
abstract function removeUnit(Unit $unit);
abstract function bombardStrength();
}
可以看到,我们为所有的 Unit 对象设计了基本功能,并且使 Army 和 TroopCarrier 两个组合对象实现这些抽象方法:
class Army {
private $units = array();
/* 用于接收Unit对象,并将对象保存在$units数组中 */
function addUnit(Unit $unit) {
if (in_array($unit, $this->units, true)) {
return;
}
array_push($this->units, $unit);
}
/* 用于移除Unit对象,使用array_udiff()获取差集实现移除数组元素 */
function removeUnit(Unit $unit) {
$this->units = array_udiff(
$this->units,
array($unit),
function($a, $b) {
return ($a==$b)?0:1;
}
);
}
/* 计算出总的工具强度 */
function bombardStrength() {
$ret = 0;
foreach ($this->units as $unit) {
$ret += $unit->bombardStrength();
}
foreach ($this->armies as $army) {
$ret += $army->bombardStrength();
}
return $ret;
}
}
Army 对象可以保存任何类型的 Unit 对象(包括 Army 对象本身,以及 ArcherUnit 对象和 LaserCannonUnit 这样的局部对象)。
现在我们再回过来看对于局部类不需要实现 add 和 remove 的方法的问题,我们可以对这些局部类在实现 add 和 remove 的方法的时候抛出异常。
/* 异常类 */
class UnitException extends Exception {}
class Archer extends Unit {
function addUnit(Unit $unit) {
throw new UnitException(get_class($this)."是一个最小单元类,不能添加其他单元!");
}
function removeUnit(Unit $unit) {
throw new UnitException(get_class($this)."是一个最小单元类,不能添加其他单元!");
}
function bombardStrength() {
return 1;
}
}
这样,我们就可以解决局部类调用 add 和 remove 的方法的问题了。为了不需要对所有局部类都进行重写 add 和 remove 的方法,我们可以在抽象类 Unit 类中即定义 add 和 remove 的方法抛出异常。
abstract class Unit {
abstract function addUnit(Unit $unit);
function addUnit(Unit $unit) {
throw new UnitException(get_class($this)."是一个最小单元类,不能添加其他单元!");
}
function removeUnit(Unit $unit) {
throw new UnitException(get_class($this)."是一个最小单元类,不能添加其他单元!");
}
}
class Archer extends Unit {
function bombardStrength() {
return 1;
}
}
这样做可以移除局部类中的重复代码,但是同时组合类不再需要强制性地实现 add 和 remove 的方法了,这可能带来问题,这在我们后面的讨论中再进一步研究。
这里我们再来看客户端的调用:
/* 添加一个Army对象 */
$main_army = new Army();
/* 添加一些Unit对象 */
$main_army->addUnit(new ArcherUnit());
$main_army->addUnit(new LaserCannonUnit());
/* 添加一个新的Army对象 */
$sub_army = new Army();
/* 添加一些Unit对象 */
$sub_army->addUnit(new ArcherUnit());
$sub_army->addUnit(new ArcherUnit());
$sub_army->addUnit(new ArcherUnit());
/* 把新的Army对象添加到第一个Army对象中 */
$main_army->addUnit($sub_army);
/* 获取army的攻击值(计算后的总攻击值) */
echo "main_army的总攻击值为: ".$main_army->bombardStrength();
在这里我们发现,调用者无需关心添加的是局部对象(ArcherUnit、LaserCannonUnit),还是组合对象(sub_army),通过 addUnit() 方法就可以实现添加作战单元的需求;同时,在调用 main_army 的总攻击值时,也无需对每一个作战单元进行计算,只需要简单地调用 bombardStrength() 方法即可,因为在幕后就已经完成对每个局部对象的攻击强度的计算。
因此我们可以归纳出以下几个优点:
- 灵活:组合模式所有子类共享一个父类型,掌握了一个子类的创建形式后,就可以轻松地在设计中添加新的组合对象或者局部对象。
- 简单:客户端在调用时没有必要区分一个对象是局部对象还是组合对象;在调用一些方法(比如bombardStrength())时,也会产生一些幕后的委托调用,而无需客户端通过循环遍历的方式完成这些调用。
10.1.3 效果
在 ArcherUnit 和 LaserCannonUnit 这样的局部类中,是不需要加入 addUnit() 和 removeUnit() 这样的冗余方法的,这样做毫无意义而且给系统设计带来歧义。但组合模式的原则便是局部类和组合类具有同样的接口,所以我们不知道一个对象是否需要 addUnit() 和 removeUnit() 这两个方法。
因此,我们创建一个 CompositeUnit 的子类继承自 Unit 父类,并且删除 Unit 父类中的 add/remove 方法:
/* 删除掉原先的add/remove方法 */
abstract class Unit {
/* 新增一个getComposite()方法 */
function getComposite() {
return null;
}
abstract function bombardStrength();
}
/* CompositeUnit 混合作战单元类
因为CompositeUnit没有实现bombardStrength()方法,
所以CompositeUnit声明为抽象类abstract
*/
abstract class CompositeUnit extends Unit {
private $units = array();
function getComposite() {
return $this;
}
protected function getUnits() {
return $this->units;
}
/* CompositeUnit类具有添加和删除单元的能力 */
function removerUnits(Unit $unit) {
$this->units = array_udiff(
$this->units,
array($unit),
function($a, $b) {
return ($a==$b)?0:1;
}
);
}
function addUnits(Unit $unit) {
if (in_array($unit, $this->units)) {
return;
}
array_push($this->units, $unit);
}
}
CompositeUnit 类继承自 Unit 类,但并没有实现 bombardStrength() 方法,所以 CompositeUnit 类也被声明为抽象的。添加的 getComposite() 方法默认返回 null,仅在 CompositeUnit 类中才返回 CompositeUnit 本身,所以,如果该方法返回一个对象,那么便可以调用它的 addUnit() 方法。
因此新的类组织形式如图:
下面是客户端调用:
class UnitScript {
/* 有两个Unit类型的参数,第一个是新的Unit对象,第二个是之前的Unit对象 */
static function joinExisting (Unit $newUnit,Unit $occupyingUnit) {
$comp;
if (!is_null($comp = $occupyingUnit->getComposite())) {
/* 如果第二个Unit对象是一个CompositeUnit对象,那么直接add */
$comp->addUnit($newUnit);
} else {
/* 如果第二个Unit对象不是一个CompositeUnit对象,那么创建一个Army对象,将两个Unit存入这个Army对象 */
$comp = new Army();
$comp->addUnit($occupyingUnit);
$comp->addUnit($newUnit);
}
return $comp;
}
}
joinExisting() 方法有两个 Unit 类型的参数,第一个是新的 Unit 对象,第二个是之前的 Unit 对象。如果第二个 Unit 对象是一个 CompositeUnit 对象,那么将第一个 Unit 对象添加给它;如果第二个 Unit 对象不是一个 CompositeUnit 对象,那么创建一个 Army 对象,将两个 Unit 存入这个 Army 对象。
在这里我们会发现简化的前提是使所有的类都继承同一个基类,但是模型变得越复杂,就不得不进行越多的类型检查。比如有一个 Cavalry(骑兵)对象,假设游戏规定不能将一匹马放到运兵船上,在组合模式我们并没有自动化的方式来强制执行这个规则,因此,我们需要在 TroopCarrier 类中完成对 Cavalry 类型的检查:
class TroopCarrier {
function addUnit(Unit $unit) {
if ($unit instanceof Cavalry) {
throw new UnitException("不能将骑兵带上船");
}
super::addUnit($unit);
}
function bombardStrength() {
return 3;
}
}
这里我们不得不使用 instanceof 来检测传给 addUnit() 方法的对象类型,特殊对象越来越多,组合模式开始渐渐显得弊大于利。
10.1.4 组合模式小结
在能够像对待单个对象一样对待组合对象的情况下,组合模式显得十分有用;但是,随着我们引入复杂的规则,代码就会变得越来越难以维护。