設計模式:靈活編程(組合模式)


組合可比繼承提供更多的靈活性。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吧ಠ╭╮ಠ。感謝閱讀,再會!!!

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM