【深入PHP 面向对象】读书笔记(八) - 让面向对象编程更加灵活的模式(一) - 组合模式

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

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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值