刚看完了《深入php面向对象、模式与实践》一书中组合模式这块内容,为了加深理解和记忆,所以着手写了这篇博客。
为方便后续理解,此处先引入两个概念,局部对象和组合对象。
局部对象:无法将其他对象组合到自身内部属性上的对象。即不能组合其他对象的对象。
组合对象:可以将其他对象组合到自身内部属性上的对象。即可以组合其他对象的对象。
注:将对象A的某个属性中存储着对象B对象的引用,则表示A与B有组合关系,其中A将B组合到了自身内部。
首先我们通过给出下面的业务需求,来引入组合模式:
业务部门想要开发一款类似于红色警戒的战斗游戏,初始局部战斗单元有两个:射手(Archer)和激光炮(LaserCannon),组合战斗单元可以有多种(由两个局部单元组合成的组合战斗单元,或者由组合单元和局部战斗单元组合成的组合战斗单元,这里随着子对象的不同,组合对象有多种)。其中组合单元和局部单元都具有战斗单元共有的属性(攻击能力,移动能力和防御能力)。而组合单元中的属性值是其子对象对应属性值的和。因为组合对象的子对象需要根据情况不断变化,那么其对应的属性值也需要不断更新和变化,这里我们需要采用什么样的组合方式考才能方便的更新和获取组合对象的属性值呢,这里就需要用到我们今天讲的组合模式了。
组合模式:
1、定义:将一组对象组合为可像单个对象一样被使用的结构。(即组合对象中获取属性值可如局部对象中一样方便)
2、分类:组合模式分为两种,透明模式和安全模式
2、实现方法:
分析:组合对象如何才可以很方便计算出属性值呢?如果可以很方便获取其子对象中的对应属性,则可以通过求和得出。那么如何方便的得到子对象中的对应属性值呢?这里主要挑战在于如何知道包含哪些子对象呢,当然最简单的方法就是在组合对象中保留子对象的引用,将所有子对象存储在一个数据结构为数组的属性中,这样我们就可以在获取属性值的方法中循环遍历该数组,然后通过求和算出组合对象的属性值。具体代码如下:
abstract class Unit { // 用来获取战斗单元的攻击属性值 abstract function bomstrength(); } // 局部战斗单元(射手) class Archer extends Unit { function bomstrength() { return 4; } } //局部战斗单元(激光炮) class LaserCannonUnit extends Unit { function bomstrength() { return 44; } } // 组合战斗单元 class Army extends Unit { private $units = array();//用来存储子对象 // 用来添加子对象 function addUnit(Unit $unit) { array_push($this->units, $unit); } // 用来计算攻击属性值 function bomstrength() { $ret = 0; foreach($this->units as $unit) { $ret += $unit->bomStrength(); } } }
上面正是满足我们需求的代码,但是更多时候客户端不需要区分对象是Army、Unit还是其他组合对象,就功能上,这些组合模式是相同的,都具有移动、攻击和防御的功能。这些特点让我们很容易想到,让他们共享同一个类型家族(这就是组合模式)。
下面是组合模式下透明和安全两种方式的实现方式:
透明模式:
abstract class Unit { abstract functon addUnit (Unit $unit); abstract function removeUnit (Unit $unit); abstract function bomStrength(); } Army class extends Unit { private $units =array(); public function addUnit(Unit $unit) { if(in_array($unit, $this->units, true)) { return; } } public function removeUnit($unit) { $this->units = array_udiff($this->units, array($unit), function($a, $b) { return ($a===$b) ?0:1}); } public function bomStrength() { $ret = 0; foreach($this->units as $unit) { $ret += $unit->bomStrength(); } } }
这里Army可以保存任何类型的Unit对象,包括Army本身或者Archer或者LaserCannon这样的局部对象。因为所有Unit对象都保证支持bomStrength方法,所以我们Army::bomStrength只需遍历$units发展,调用每个Unit对象的bomStrength方法,就可以计算出带动军队的攻击强度了。
但是实际上,我们不需要在Archer上添加Unit对象,所以当Archer或者LaserCannon这种局部对象调用addUnit或者removeUnit方法时需要抛出异常,我们可以在抽象类中指定这两个方法抛出异常,只在组合对象中重写这两个函数。这种就是组合模式中的透明模式,特点就是对客户端透明,无论是局部对象还是组合对象,方法是公有的,不过这里有很明显的缺点:当局部对象调用removeUnit或者addUnit方法时虽然方法存在,但是只会给出异常,这是我们在运行中不想遇到的,有没有方法可以实现共用一个父类,同时不会出现不可预期的报错行为的方法呢。当然有,这就量组合模式的另一种模式,安全模式。
安全模式:
abstract class Unit { function getComposite() { return null; } abstract function bomStrength(); } class CompositeUnit extends Unit { private $units = array(); function getComposite() { return $this; } protected function units() { return $this->units; } public function removeUnit(Unit $unit) { $this->units = array_udiff($this->units, array($unit), function($a, $b) {return ($a===$b)?0:1} ); } public function addUnit(Unit $unit) { if(in_array($unit, $this->units, true)) { return; } $this->units[] = $unit; } }
如上所示:我们为组合对象添加了一个子抽象类,这个子抽象类中多了个方法getComposite,这个方法用于客户端识别是否为组合对象,如果是的话,返回这个 对象,如果不是,则返回null,这样客户端在调用aaUnit或者removeUnit方法前调用getComposite方法即可知道对象的类型,并作出适当的操作,完美的解决了透明模式中出现的问题。
但是这两种模式本身没有孰优孰劣,具体使用哪种,需要根据业务来区分。
需要担心组合模式的成本如果子类嵌套太多,可能一个循环就把系统搞崩了,所以这里给出使用组合模式的技巧:
1)需要在父集对象中将子类的属性值缓存下来,这样可以减少系统开销,即便如此,还需要保证缓存值不会过期,即需要实时更新过期缓存。
2)对象持久化上,组合模式不适合存储在关系型数据库中,因为这样随着系统嵌套的深度加大,sql查询的开销就会越大,这样更新添加和移除子对象时的系统开销就会比较大。不过组合模式中的对象关系适合存放在xml中,xml中树形结构和组合对象中的树形结构正好匹配。
好了,关于组合模式的个人理解就讲到这了,内容会不断更新,力求慢慢领悟各种模式的真谛。