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

开篇

若是你注意了目录,会知道:组合是一个新的开始。
在系统代码设计的过程当中,咱们经过继承来组织代码,父类与子类,实质上对应了业务的总体规范与具体需求。因此,咱们须要将类按照某种逻辑组合起来,从而让类成为一个集合化的体系。
组合模式,描述的就是这种逻辑——当咱们须要经过规范的操做,来联系一些类,甚至将其格式化为父子层级关系时,咱们有哪些模式(“工具”)可用。数据库

问题

管理一组对象的复杂性比较高,从外部经过理论的方式去诠释它,难度更大。为此,这里设计一个虚构场景:
在前面的模式中,咱们使用了一个相似文明游戏的场景,如今继续使用它,在这里,咱们要实现一个简易的战斗单位组成系统。segmentfault

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

abstract class Unit {
    abstract function bombardStrength();
}

class Archer extends Unit {
    function bombardStrength()
    {
        return 3;
    }
}

class LaserCannonUnit extends Unit {
    function bombardStrength()
    {
        return 10;
    }
}

咱们设计了一个抽象方法bombardStrength,用于设置战斗单位的伤害,而且经过继承实现了两个具体的子类:ArcherLaserCannonUnit,完整的类天然应该包含移动速度、防护等内容,但你能发现这是同质化的,因此咱们为了示例代码的简单,省略掉它。安全

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

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方法来计算总伤害。但我想若是你对游戏有兴趣,就不会知足于这样一个粗糙的模型,咱们来添点新东西:我军/盟军拆分(目前它们若是混合在一块儿,就没法再区分部队归属)oop

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,而且在逻辑上,也具有共同点,因此咱们能够将其整合为一个类的家族。this

实现

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

能够看到,全部的军队类都源于Unit,但这里有一个注解:ArmyTroopCarrier类为组合对象ArcherLaserCannon类则是局部对象树叶对象设计

这里额外描述一下组合模式的类结构,它是一种树形结构,组合对象为枝干,能够开出至关数量的叶子,树叶对象则是最小单位,其内部没法包含本组合模式的其余对象。

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

下面咱们开始实现UnitArmy类,观察Army能够发现,它能够保存全部的Unit衍生的类实例(对象),由于它们具有相同的方法,须要军队的攻击强度,只要调用攻击强度方法,就能够完成汇总。

如今,咱们面对的一个问题是:如何实现addremove方法,通常组合模式会在父类中添加这些方法,这确保了全部衍生类共享同一个接口,但同时表示:系统设计者将容忍冗余。

这是默认实现方法:

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

咱们能够作一些小改进:将addremove的抛出异常代码挪入父类:

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,额外的,你可能喜欢“直接继承”、“间接继承”的概念)

小疑惑

我发现,外国人每每采用实际应用来教学,尤为是游戏之类的很是有趣的应用,不知这是国外教学的传统,仍是我错位的理解。

相关文章
相关标签/搜索