【面向對象的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,額外的,你可能喜歡「直接繼承」、「間接繼承」的概念)

小疑惑

我發現,外國人每每採用實際應用來教學,尤爲是遊戲之類的很是有趣的應用,不知這是國外教學的傳統,仍是我錯位的理解。

相關文章
相關標籤/搜索