若是你注意了目錄,會知道:組合是一個新的開始。
在系統代碼設計的過程當中,咱們經過繼承來組織代碼,父類與子類,實質上對應了業務的總體規範與具體需求。因此,咱們須要將類按照某種邏輯組合起來,從而讓類成爲一個集合化的體系。
組合模式,描述的就是這種邏輯——當咱們須要經過規範的操做,來聯繫一些類,甚至將其格式化爲父子層級關係時,咱們有哪些模式(「工具」)可用。數據庫
管理一組對象的複雜性比較高,從外部經過理論的方式去詮釋它,難度更大。爲此,這裏設計一個虛構場景:
在前面的模式中,咱們使用了一個相似文明遊戲的場景,如今繼續使用它,在這裏,咱們要實現一個簡易的戰鬥單位組成系統。segmentfault
先定義一些戰鬥單元的類型:緩存
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
方法來計算總傷害。但我想若是你對遊戲有興趣,就不會知足於這樣一個粗糙的模型,咱們來添點新東西:我軍/盟軍拆分(目前它們若是混合在一塊兒,就沒法再區分部隊歸屬)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:spa
能夠看到,全部的軍隊類都源於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,額外的,你可能喜歡「直接繼承」、「間接繼承」的概念)
我發現,外國人每每採用實際應用來教學,尤爲是遊戲之類的很是有趣的應用,不知這是國外教學的傳統,仍是我錯位的理解。