摘要:我們可以做一些小改進將的拋出異常代碼挪入父類屬于最小單位。完整代碼當我們需要在某個子類,實現個性化的業務邏輯時,組合模式的缺陷之一正在顯現出來簡化的前提是所有的類都繼承同一個基類,簡化優點有時是以降低對象安全為代價。
開篇
如果你注意了目錄,會知道:組合是一個新的開始。
在系統代碼設計的過程中,我們通過繼承來組織代碼,父類與子類,實質上對應了業務的整體規范與具體需求。所以,我們需要將類按照某種邏輯組合起來,從而讓類成為一個集合化的體系。
組合模式,描述的就是這種邏輯——當我們需要通過規范的操作,來聯系一些類,甚至將其格式化為父子層級關系時,我們有哪些模式(“工具”)可用。
管理一組對象的復雜性比較高,從外部通過理論的方式去詮釋它,難度更大。為此,這里設計一個虛構場景:
在前面的模式中,我們使用了一個類似文明游戲的場景,現在繼續使用它,在這里,我們要實現一個簡易的戰斗單位組成系統。
先定義一些戰斗單元的類型:
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:
可以看到,所有的軍隊類都源于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,額外的,你可能喜歡“直接繼承”、“間接繼承”的概念)
我發現,外國人往往采用實際應用來教學,尤其是游戲之類的非常有趣的應用,不知這是國外教學的傳統,還是我錯位的理解。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/22947.html
摘要:系列目錄生成對象的模式面向對象的之模式單例面向對象的之模式工廠方法面向對象的之模式抽象工廠方法面向對象的之模式原型組合類對象的模式面向對象的之模式組合更新裝飾模式 系列目錄 生成對象的模式 【面向對象的PHP】之模式:單例 【面向對象的PHP】之模式:工廠方法 【面向對象的PHP】之模式:抽象工廠方法 【面向對象的PHP】之模式:原型 組合類/對象的模式 【面向對象的PHP】之模式...
摘要:我們今天也來做一個萬能遙控器設計模式適配器模式將一個類的接口轉換成客戶希望的另外一個接口。今天要介紹的仍然是創建型設計模式的一種建造者模式。設計模式的理論知識固然重要,但 計算機程序的思維邏輯 (54) - 剖析 Collections - 設計模式 上節我們提到,類 Collections 中大概有兩類功能,第一類是對容器接口對象進行操作,第二類是返回一個容器接口對象,上節我們介紹了...
摘要:例如汽車這個名詞可以理解為汽車的總類,但這輛寶馬汽車則是一個具體的汽車對象。當在類成員方法內部調用的時候,可以使用偽變量調用當前對象的屬性。在面向對象中則被稱之為方法。 簡述 現在大伙都在講面向對象編程,但是我們也得先找著一個對象是不?不然怎么面向對象?怎么編程? --- 笑話一則,但是理不虧,要搞P面向對象編程,我們起碼要先搞懂對象(還有類)是什么?只有了解它,理解它,你才能駕馭它。...
摘要:使用和在中,通過為屬性或方法設置和關鍵字可以實現對屬性或方法的可見性控制。你的繼承表達了一個對等比如人類是動物的關系,不是包含的關系比如用戶具有用戶詳情你能從基類中復用代碼你想通過修改全局類來對所有派生類進行修改。 使用getter和setter 在 PHP 中,通過為屬性或方法設置 public, protected 和 private 關鍵字可以實現對屬性或方法的可見性控制。不過,...
閱讀 5265·2021-09-22 15:59
閱讀 1856·2021-08-23 09:42
閱讀 2561·2019-08-29 18:42
閱讀 3444·2019-08-29 10:55
閱讀 2058·2019-08-27 10:57
閱讀 1759·2019-08-26 18:27
閱讀 2722·2019-08-23 18:26
閱讀 2912·2019-08-23 14:40