組合可比繼承提供更多的靈活性。composition provides greater flexibility than inheritance. -- 《深入PHP 面向對象、模式與實踐》
介紹
組合模式可以很好地聚合和管理許多相似的對象,因而對客戶端代碼來說,一個獨立對象和一個對象集合是沒有差別的(部分-整體)。組合模式定義了一個單根繼承體系,使具有截然不同職責的集合可以並肩工作。
簡單來說,組合模式是運用面向對象的方式來有效的處理樹形結構,如下圖:
- Component :抽象構件,樹形結構的根節點,為組合對象聲明接口,也可以為共有接口實現缺省行為。
- Leaf :樹形結構的葉節點,該節點沒有子節點,實現抽象構件所聲明的接口。
- Compsite :樹形結構的樹枝節點,該節點有子節點,實現抽象構件所聲明的接口,存儲子部件。
下圖為UML類圖:
問題
先構建一個虛擬場景:統計公司的職工數量。先定義幾個模型:
abstract class Department { abstract function employees(); } class SalesDepartment extends Department { public function employees() { return 200; } } class TechnologyDepartment extends Department { public function employees() { return 123; } }
Department 類定義了一個抽象方法 employees ,用於返回當前部門的職工數,然后再兩個部門類 SalesDepartment 、 TechnologyDepartment 實現了 employees 方法。現在我們可以定義一個單獨的類來合並職工數:
class Company { private $_departments = array(); public function addDepartment(Department $department) { array_push($this->_departments, $department); } public function employees() { $tem = 0; foreach ($this->_departments as $department) { $tem += $department->employees(); } return $tem; } }
Company 有兩 個方法 addDepartment 、 employees 。 addDepartment 方法用於接受 Department 對象並保存到 $_departments 中。 employees 方法通過一個簡單的迭代遍歷所聚合的 Department 對象並調用 employees 方法,計算出總的職工數。
當前模型對現有需求還是可以很好的滿足的,但是如果這個公司又成立了分公司,需要將分公司的職工數也統計進來,並且還能夠拆離出來又怎樣呢?
我們修改 Company 類,使之可以像添加 Department 對象一樣添加 Company 對象:
class Company { private $_company = array(); private $_departments = array(); public function addCompany(Company $company) { array_push($this->_company, $company); } public function addDepartment(Department $department) { array_push($this->_departments, $department); } public function employees() { $tem = 0; foreach ($this->_company as $company) { $tem += $company->employees(); } foreach ($this->_departments as $department) { $tem += $department->employees(); } return $tem; } }
現在來看這個類還算不太復雜,但是隨着需求的增加,這個類所提供的功能也會越來越多。我們回過頭來看上面的這幾個類,都需要有 employees 方法,無論是 SalesDepartment 、 TechnologyDepartment 還是 Company 它們所提供的功能是相同的,統計職工數。這些相似性給我們帶來一個必然的結論:因為容器對象與它們包含的對象共享同一個接口,所以它們應該共享同一個類型家族。
實現
abstract class Unit { abstract function addUnit(Unit $unit); abstract function removeUnit(Unit $unit); abstract function employees(); }
class Company extends Unit{ private $_units = array(); public function addUnit(Unit $unit) { array_push($this->_units, $unit); } public function removeUnit(Unit $unit) { $this->_units = array_udiff($this->_units, array($unit), function ($a, $b){ return ($a === $b) ? 0 : 1; }); } public function employees() { $tem = 0; foreach ($this->_units as $unit) { $tem += $unit->employees(); } return $tem; } }
Company 可以保存任何類型的 Unit 對象,包括它自己本身。這樣保證了每個對象都支持 Unit 定義的方法。先看下調用:
$main_army = new Company(); $main_army->addUnit(new SalesDepartment()); $main_army->addUnit(new TechnologyDepartment()); $sub_army = new Company(); $sub_army->addUnit(new SalesDepartment()); $sub_army->addUnit(new SalesDepartment()); $sub_army->addUnit(new SalesDepartment()); $main_army->addUnit($sub_army); $main_army->employees();
可以看到我們只需簡單的操作就能計算出總的職工數了,組合模式將計算復雜性完全隱藏了。並且符合組合模式的原則:局部類和組合類具有相同的接口。
優化
我們發現,由於 SalesDepartment 和 TechnologyDepartment 並不需要 addUnit 和 removeUnit 方法,但是 Unit 將兩個方法定義為抽象方法,則子類必須實現,這就導致了代碼冗余。所以,需要解決此問題:
定義默認方法
Unit 中定義默認方法,這樣在子類中不必要實現 addUnit 和 removeUnit 方法。
abstract class Unit { abstract function employees(); public function addUnit(Unit $unit) { throw new UnitException('error msg'); } public function removeUnit(Unit $unit) { throw new UnitException('error msg'); } }
雖然定義了默認方法,非法調用就會拋出異常,但是這么處理未必就是好的,我們仍然不知道調用 Unit 對象的 addUnit 方法是否是安全的。因此,采用這種處理方式就要權衡下了。
優化模式結構
雖然可以將添加和刪除方法放到局部類中定義,但是這么處理就需要在每個局部類中分開定義,沒有統一約束。所以,將基類 Unit 分解為 CompositeUnit 子類型,然后由局部類來繼承。
abstract class Unit { abstract function employees(); public function getComposite() { return null; } }
abstract class CompositeUnit extends Unit { private $_units = array(); public function addUnit(Unit $unit) { if (in_array($unit, $this->_units, true)) { return; } $this->_units[] = $unit; } public function removeUnit(Unit $unit) { $this->_units = array_udiff($this->_units, array($unit), function ($a, $b){ return ($a === $b) ? 0 : 1; }); } public function getComposite() { return $this; } }
可以發現新增了個 getComposite 方法, getComposite 方法是用來區分能否調用 addUnit 、 removeUnit 方法的,如果 getComposite 返回不是NULL就說明方法可調用。
這樣看起來結構更清晰了,這種結構還可以再優化,這就需要在項目中慢慢調整了。最切合需求的才是最好的!
總結
組合模式能使原本復雜的計算簡單化,在適合的業務需求中使用會帶來很多益處:
- 靈活:因為組合模式中的一切類都共享了同一個父類型,所以可以輕松地在設計中添加新的組合對象或者局部對象,而無需大范圍地修改代碼。
- 簡單:使用組合結構的客戶端代碼只需設計簡單的接口。客戶端代碼沒有必要區分一個對象是組合對象還是局部對象(除了添加新組件時)。
- 隱式到達:組合模式中的對象通過樹型結構組織。每個組合對象中都保存着對子對象的引用。因此對樹中某部分的一個小操作可能會產生很大的影響。
- 顯式到達:樹型結構可輕松遍歷。可以通過迭代樹型結構來獲取組合對象和局部對象的信息,或對組合對象和局部對象執行批量處理。
但另一方面,組合模式又依賴於其組成部分的簡單性。隨着我們引入復雜的規則,代碼會變得越來越難以維護。
后記
設計模式真是看似簡單,其實復雜的東西,理解實現原理很容易,但是想要活用,就要理解模式的思想了。本文部分摘錄自《深入PHP 面向對象、模式與實踐》,書很好,我看的倒是挺慢的,有興趣的也可以讀讀。最后吐槽下用 Visio 畫UML圖真不太方便,還是用回Astah Professional吧ಠ╭╮ಠ。感謝閱讀,再會!!!