【深入PHP 面向对象】读书笔记(八)

来源:互联网 发布:python可以建网站吗 编辑:程序博客网 时间:2024/05/25 05:36

【简介】组合比继承具有更大的灵活性。
组合模式:将一组对象组合为可像单个对象一样被使用的结构。
装饰模式:通过在运行时合并对象来扩展功能呢的一种灵活机制。
外观模式:为复杂或多变的系统创建一个简单的接口。

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() 方法即可,因为在幕后就已经完成对每个局部对象的攻击强度的计算。

因此我们可以归纳出以下几个优点:

  1. 灵活:组合模式所有子类共享一个父类型,掌握了一个子类的创建形式后,就可以轻松地在设计中添加新的组合对象或者局部对象。
  2. 简单:客户端在调用时没有必要区分一个对象是局部对象还是组合对象;在调用一些方法(比如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 组合模式小结

在能够像对待单个对象一样对待组合对象的情况下,组合模式显得十分有用;但是,随着我们引入复杂的规则,代码就会变得越来越难以维护。

阅读全文
0 0