php对象合并,【面向对象的PHP】之模式:组合

本文探讨了组合模式在游戏开发中的使用,通过一个战斗单位组成系统的例子展示了如何利用组合模式来组织代码。文章介绍了如何通过组合模式实现军队的增删和计算总伤害,并分析了组合模式带来的灵活性、简单性和隐式到达的效果。同时,文章指出组合模式的局限性,如复杂对象引入的安全问题、操作成本和对象持久化的困难,并提出了一些解决方案。最后,文章以一个小型测试来说明组合模式的实现和效果。
摘要由CSDN通过智能技术生成

开篇

如果你注意了目录,会知道:组合是一个新的开始。

在系统代码设计的过程中,我们通过继承来组织代码,父类与子类,实质上对应了业务的整体规范与具体需求。所以,我们需要将类按照某种逻辑组合起来,从而让类成为一个集合化的体系。

组合模式,描述的就是这种逻辑——当我们需要通过规范的操作,来联系一些类,甚至将其格式化为父子层级关系时,我们有哪些模式(“工具”)可用。

问题

管理一组对象的复杂性比较高,从外部通过理论的方式去诠释它,难度更大。为此,这里设计一个虚构场景:

在前面的模式中,我们使用了一个类似文明游戏的场景,现在继续使用它,在这里,我们要实现一个简易的战斗单位组成系统。

先定义一些战斗单元的类型:

abstract class Unit {

abstract function bombardStrength();

}

class Archer extends Unit {

function bombardStrength()

{

return 3;

}

}

class LaserCannonUnit extends Unit {

function bombardStrength()

{

return 10;

}

}

我们设计了一个抽象方法bombardStrength,用于设置战斗单位的伤害,并且通过继承实现了两个具体的子类:Archer、LaserCannonUnit,完整的类自然应该包含移动速度、防御等内容,但你能发现这是同质化的,所以我们为了示例代码的简单,省略掉它。

下面,我们创建一个独立类,来实现战斗单元的组合(军队)。

class Army {

private $units = array();

function addUnit( Unit $unit ) {

array_push($this->units, $unit);

}

function bombradStrength() {

$ret = 0;

foreach ($this->units as $unit) {

$ret += $unit->bombardStrength();

}

return $ret;

}

}

Army类的addUnit方法用于接收单位,通过bombardStrength方法来计算总伤害。但我想如果你对游戏有兴趣,就不会满足于这样一个粗糙的模型,我们来添点新东西:我军/盟军拆分(目前它们如果混合在一起,就无法再区分部队归属)

class Army {

private $units = array();

private $armies = array();

function addUnit( Unit $unit ) {

array_push($this->units, $unit);

}

function addArmy( Army $army ) {

array_push( $this->armies, $army );

}

function bombradStrength() {

$ret = 0;

foreach ($this->units as $unit) {

$ret += $unit->bombardStrength();

}

foreach ( $this->armies as $army ) {

$ret += $army->bombardStrength();

}

return $ret;

}

}

所以现在,这个Army类不但可以合并军队,更可以在需要时,将处于一支军队的盟我部队拆分开。

最后,我们观察写好的这些类,他们都具备同一个方法bombardStrength,并且在逻辑上,也具备共同点,所以我们可以将其整合为一个类的家族。

实现

组合模式采用单根继承,下面放出UML:

ced08a1b65b545ae60bd33362d4d43f4.png

可以看到,所有的军队类都源于Unit,但这里有一个注解:Army、TroopCarrier类为组合对象,Archer、LaserCannon类则是局部对象或树叶对象。

这里额外描述一下组合模式的类结构,它是一种树形结构,组合对象为枝干,可以开出相当数量的叶子,树叶对象则是最小单位,其内部无法包含本组合模式的其他对象。

这里有一个问题:局部对象是否需要包含addUnit、removeUnit之类的方法,在这里我们为了保持一致性,后面再讨论。

下面我们开始实现Unit、Army类,观察Army可以发现,它可以保存所有的Unit衍生的类实例(对象),因为它们具备相同的方法,需要军队的攻击强度,只要调用攻击强度方法,就可以完成汇总。

现在,我们面对的一个问题是:如何实现add、remove方法,一般组合模式会在父类中添加这些方法,这确保了所有衍生类共享同一个接口,但同时表示:系统设计者将容忍冗余。

这是默认实现方法:

class UnitException extends Exception {}

abstract class Unit {

abstract function addUnit( Unit $unit );

abstract function removeUnit( Unit $unit );

abstract function bombardStrength();

}

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 3;

}

}

class Army extends Unit {

private $units = array();

function addUnit( Unit $unit ) {

if ( in_array( $unit, $this->units ,true)) {

return;

}

$this->units[] = $unit;

}

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();

}

return $ret;

}

}

我们可以做一些小改进:将add、remove的抛出异常代码挪入父类:

abstract class Unit {

function addUnit( Unit $unit ) {

throw new UnitException( get_class($this) . " 属于最小单位。");

}

function removeUnit( Unit $unit ) {

throw new UnitException( get_class($this) . " 属于最小单位。");

}

abstract function bombardStrength();

}

class Archer extends Unit {

function bombardStrength()

{

return 3;

}

}

组合模式的益处

灵活:组合模式中的所有类都共享了同一个父类型,所以可以轻松的在设计中添加新的组合对象或局部对象,而无需大范围修改代码。

简单:使用组合模式,客户端代码只需设计简单的接口。对客户端来说,调用需要的接口即可,不会出现任何“调用不存在接口”的情况,最少,它也会反馈一个异常。

隐式到达:对象通过树形结构组织,每个组合对象都保存着对子对象的引用,因此,书中某部分的一个小操作,可能会产生很大影响,却不为人知——譬如:我们将军队1名下的一支军队(a),挪动到军队2,实际上挪动的是军队(a)中所有的军队个体。

显示到达:树形结构可以轻松遍历,可以快捷的通过迭代树形结构,来获取包含对象的信息。

最后,我们做一个Small Test吧。

// 创建番号

$myArmy = new Army();

// 添加士兵

$myArmy->addUnit( new Archer() );

$myArmy->addUnit( new Archer() );

$myArmy->addUnit( new Archer() );

// 创建番号

$subArmy = new Army();

// 添加士兵

$subArmy->addUnit( new Archer() );

$subArmy->addUnit( new Archer() );

$myArmy->addUnit( $subArmy );

echo "MyArmy的合计伤害为:" . $myArmy->bombardStrength(); // MyArmy的合计伤害为:15

效果

来让我解释一下:为何addUnit之类的方法,必须出现在局部类中,因为我们要保持Unit的透明性——客户端在进行任何访问时,都清楚的知道:目标类中肯定有addUnit或其他方法,而不需要去猜疑。

现在,我们将Unit类解析出一个抽象子类CompositeUnit,并将组合对象具备的方法挪到它身上,加入监测机制:getComposite。

现在,我们解决了“冗余方法”,只是我们每次调用,都必须通过getComposite确认是否为组合对象,并且按照这种逻辑,我们可以写一段测试代码。

完整代码:

class UnitException extends Exception {}

abstract class Unit {

function getComposite() {

return null;

}

abstract function bombardStrength();

}

abstract class CompositeUnit extends Unit {

private $units = array();

function getComposite() {

return $this;

}

protected function units() {

return $this->units;

}

function addUnit( Unit $unit ) {

if ( in_array( $unit, $this->units ,true)) {

return;

}

$this->units[] = $unit;

}

function removeUnit( Unit $unit ) {

$this->units = array_udiff(

$this->units,

array( $unit ),

function( $a, $b ) { return ($a === $b) ? 0 : 1; }

);

}

}

class UnitScript {

static function joinExisting( Unit $newUnit, Unit $occupyingUnit ) {

if ( !is_null( $comp = $occupyingUnit->getComposite() ) ) {

$comp->addUnit( $newUnit );

} else {

$comp = new Army();

$comp->addUnit( $occupyingUnit );

$comp->addUnit( $newUnit );

}

return $comp;

}

}

当我们需要在某个子类,实现个性化的业务逻辑时,组合模式的缺陷之一正在显现出来:简化的前提是所有的类都继承同一个基类,简化优点有时是以降低对象安全为代价。为了弥补损失的安全,我们需要进行类型检查,直到有一天你会发现:我们做了太多的检查工作——甚至已经开始显著影响到代码效率。

class TroopCarrier {

function addUnit(Unit $unit) {

if ($unit instanceof Cavalry) {

throw new UnitException("不能将马放置于船上");

super::addUnit($unit);

}

}

function bombardStrength() {

return 0;

}

}

组合模式的优点正在不断被数量越来越多的特殊对象所冲抵,只有在大部分局部对象可互换的情况下,组合模式才最适用。

另一个揪心的问题是:组合对象的操作成本,如果你玩过最高指挥官或者横扫千军,就会明白这个问题的严重性,当你拥有了上千个战斗单位,并且这些单位本身还分别属于不同的番号,你每次计算某个军队数值,都会带来庞大的军队开销,甚至是系统崩溃。

我相信我们都能想到:在父级或最高级对象中,保存一个缓存,这样的解决方法,但实际上除非你用精度极高的浮点数,否则要小心缓存的有效性(尤其是像JS一类的语言,为了做一系列的游戏数值缓存,我曾忽略了它的数值换算误差)。

最后,对象持久化上需要注意:1. 虽然组合模式是一个优雅的模式,但它并不能将自身轻松的存储到关系型数据库中,你需要通过多个昂贵的查询,来将整个结构保存在数据库中;2. 我们可以通过赋予ID来解决 1. 的问题,可仍需要在获取对象后,重建父子引用关系,这会让它变得略显混乱。

小结

如果你想“如同操作一个对象般的随心所欲”,那么组合模式的是你所需要的。

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

额外的:组合模式不能很好地保存在关系数据库,但却非常适合使用XML进行持久化。

(持久化 = 保存)

(父类 = 超类,因为英文都是SuperClass,额外的,你可能喜欢“直接继承”、“间接继承”的概念)

小疑惑

我发现,外国人往往采用实际应用来教学,尤其是游戏之类的非常有趣的应用,不知这是国外教学的传统,还是我错位的理解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值