1. 程式人生 > >設計模式:對象生成(單例、工廠、抽象工廠)

設計模式:對象生成(單例、工廠、抽象工廠)

添加 對象實例 log return ray 靜態 學習 線程 tco

對象的創建有時會成為面向對象設計的一個薄弱環節。我們可以使用多種面向對象設計方案來增加對象的創建的靈活性。
  • 單例模式:生成一個且只生成一個對象實例的特殊類
  • 工廠方法模式:構建創建者類的繼承層級
  • 抽象工廠模式:功能相關產品的創建

1. 單例模式

  全局變量是面向對象程序員遇到的引發bug的主要原因之一。全局變量將類捆綁於特定的環境,破壞了封裝。如果新的應用程序無法保證一開始就定義了相同的全局變量,那麽一個依賴於全局變量的類就無法從一個應用程序中提取出來並應用到新的應用程序中。這些問題的產生也引發了單例模式的出現,保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。

  單例模式是最簡單的設計模式之一,也是被使用較多的設計模式。這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。

1 單例類對象可以被系統中的任何對象使用。
2 單例類對象不應該被儲存在會被覆寫的全局變量中。
3 單例類對象在系統中不應該超過一個。

  首先,我們構建一個類 Preferences,先要求這個類無法從其自身外部來創建實例的類。實現這個只需定義一個私有的構造方法即可:

 class Preferences 
 {
     private $_props = array();
     
     private function __construct() {} // 禁止從外部創建類實例
     
     public function setProperty($key
, $val) { $this->_props[$key] = $val; } public function getProperty($key) { return $this->_props[$key]; } }

  現在這個類不能從外部類創建實例,但是內部也沒有對外提供獲取實例的方法,所以這個類是不能用的。這裏我們添加靜態方法和靜態屬性來間接實例化對象:

class Preferences
{
    private $_props = array();
    private
static $_instance = null; private function __construct() {} public static function getInstance() { if (empty(self::$_instance)) { self::$_instance = new Preferences(); } return self::$_instance; } public function setProperty($key, $val) { $this->_props[$key] = $val; } public function getProperty($key) { return $this->_props[$key]; } }

  這是一個最基本的單例模式,我們可以通過靜態方法 getInstance 來獲取 Preferences 類的實例對象,同時能夠保證該對象在系統中是唯一的。測試一下:

$pref1 = Preferences::getInstance();
$pref1->setProperty(‘name‘, ‘bndong‘);
var_dump($pref1);
unset($pref1); $pref2 = Preferences::getInstance(); var_dump($pref2); var_dump($pref2->getProperty(‘name‘));

  打印結果:

object(Preferences)[1]
  private ‘_props‘ => 
    array (size=1)
      ‘name‘ => string ‘bndong‘ (length=6)
object(Preferences)[1] private ‘_props‘ => array (size=1) ‘name‘ => string ‘bndong‘ (length=6)
string ‘bndong‘ (length=6)

  可以看到 $pref1 和 $pref2 的對象標識符相同,並且 $pref2 可以獲取到 $pref1 寫入的值,也就說明二者所指向的對象為一個。實現了單例模式的需求:

技術分享圖片

 註意:

  1. 上面實現單例模式使用的是PHP,所以不需要考慮線程安全的問題, Java 實現單例模式就需要考慮線程安全問題了,可以使用 synchronized 狀態修飾來進行控制,這裏我不過多贅述,想了解的可以去網上查閱。
  2. 在PHP中,所有的變量無論是全局變量還是類的靜態成員,都是頁面級的,每次頁面被執行時,都會重新建立新的對象,都會在頁面執行完畢後被清空。所以PHP單例模式我覺得只是針對單次頁面級請求時出現多個應用場景並需要共享同一對象資源時是非常有意義的。
  3. 單例和全局變量都可能被誤用。因為單例在系統任何地方都可以被訪問,所以它們可能會導致很難調試的依賴關系。如果改變一個單例,那麽所有使用該單例的類都會受到影響。在這裏,依賴本身不是問題。畢竟,我們在每次聲明一個有特定類型參數的方法時,也就創建了依賴關系。問題是,單例對象的全局化的性質會使程序員繞過類接口定義的通信線路。當單例被使用時,依賴便會被隱藏在方法內部,而並不會出現在方法聲明中。這使得系統中的依賴關系更加難以追蹤,因此需要謹慎小心地部署單例類。

 另外(無關緊要),由於項目畢竟是一個團隊開發,我在實現核心類的單例模式時候一般對類外加一些限制:

  1. 使用 final 對類進行限制,防止類被繼承或覆蓋。
  2. 私有化 __clone() 防止克隆。

2. 工廠方法模式

  工廠方法模式也是一種常用的對象創建型設計模式,這種模式是用特定的類來處理實例化,通過依賴註入達到解耦,解決了當代碼關註於抽象類型時如何創建對象實例的問題。

  工廠方法模式是對簡單工廠模式的改進,首先 簡單工廠模式:專門定義一個類用來負責創建其他類的實例,被創建的實例通常都具有共同的父類。

abstract class ApptEncoder
{
    abstract function encode();
}

class BloggsApptEncoder extends ApptEncoder 
{
    public function encode()
    {
        // TODO: Implement encode() method.
    }
}

class MegaApptEncoder extends ApptEncoder 
{
    public function encode()
    {
        // TODO: Implement encode() method.
    }
}

class CommsManager
{
    const BLOGGS = 1;
    const MEGA   = 2;
    
    private $_mode = 1;
    
    public function __construct($mode)
    {
        $this->_mode = $mode;
    }


    public function getApptEncoder()
    {
        switch ($this->_mode) {
            case self::BLOGGS :
                return new MegaApptEncoder();
            default:
                return new BloggsApptEncoder();
        }
    }
}

$comms = new CommsManager(CommsManager::BLOGGS);
$apptEncoder = $comms->getApptEncoder();
$apptEncoder->encode();

技術分享圖片

  我們通過簡單工廠模式的結構可以發現:(適用於工廠類負責創建對象較少的情形)

  1. 工廠類中包含了所有實例的創建邏輯,一旦這個工廠不能工作,整個系統都會受到影響。
  2. 違背開放關閉原則,一旦添加新的工廠子類就不得不修改工廠類的邏輯。
  3. 如果添加新的方法會迫使我們要重復使用條件判斷語句,維護麻煩,而且當條件語句蔓延到代碼中,我們不應該感到樂觀。

  為了解決這些問題,衍生出了設計模式:工廠方法模式

  工廠方法模式的工廠類,不再負責所有類的實例創建,不會因為添加新的工廠子類而修改工廠類的邏輯,符合開放閉合原則。

  把 CommsManager 重新制定為抽象類。這樣可以得到一個靈活的父類,並把所有特定協議相關代碼方法具體的子類中:

abstract class ApptEncoder
{
    abstract function encode();
}

class BloggsApptEncoder extends ApptEncoder
{
    public function encode()
    {
        // TODO: Implement encode() method.
    }
}

class MegaApptEncoder extends ApptEncoder
{
    public function encode()
    {
        // TODO: Implement encode() method.
    }
}

abstract class CommsManager
{
    abstract function getA();
    abstract function getB();
    abstract function getApptEncoder();
}

class BloggsCommsManger extends CommsManager
{
    public function getA()
    {
        // TODO: Implement getA() method.
    }

    public function getB()
    {
        // TODO: Implement getB() method.
    }

    public function getApptEncoder()
    {
        return new BloggsApptEncoder();
    }
}

class MegaCommsManger extends CommsManager
{
    public function getA()
    {
        // TODO: Implement getA() method.
    }

    public function getB()
    {
        // TODO: Implement getB() method.
    }

    public function getApptEncoder()
    {
        return new MegaApptEncoder();
    }
}

技術分享圖片

  這裏簡單介紹下上面代碼的結構,理解了的請略過

  • 工廠類:CommsManager 用於定義工廠子類
  • 工廠子類:MegaCommsManager、BloggsCommsManager 用於創建每個產品對象
  • 產品:ApptEncoder 用於定義產品子類
  • 產品子類:MegaApptencoder、BloggsApptEncoder

  可以看到在工廠類中增加了兩個方法:getA,getB。這也是工廠方法模式所適用的一種情形, 如果只為創建子類就實現工廠方法模式就需要再考慮下了。套用我上級老大的一句話:永遠不要為了模式而模式

  工廠方法模式,解決了許多簡單工廠模式的問題,遵循開放閉合原則,可擴展。但是這種模式也形成了一種特殊的代碼重復,因而不被一些人喜歡。並且添加新產品時,處理增加新“產品”類外,還要提供與之對應的具體工廠類,系統類的個數成對增加。工廠方法模式可以解決類的橫向擴展,對於類的縱向擴展並不能有效解決,這也就引出了抽象工廠模式。

3. 抽象工廠模式

  抽象工廠解決了工廠方法縱向擴展問題,例如我們縱向增加了一些類:

 技術分享圖片

  如果使用工廠方法模式來實現

abstract class CommsManager
{
    abstract function getA();
    abstract function getB();
    abstract function getApptEncoder();
    abstract function getTtdEncoder();
    abstract function getContactEncoder();
}

class BloggsCommsManger extends CommsManager
{
    public function getA()
    {
        // TODO: Implement getA() method.
    }

    public function getB()
    {
        // TODO: Implement getB() method.
    }

    public function getApptEncoder()
    {
        return new BloggsApptEncoder();
    }
    
    public function getTtdEncoder()
    {
        return new BloggsTtdEncoder();
    }
    
    public function getContactEncoder()
    {
        return new BloggsContactEncoder();
    }
}

技術分享圖片

  上面是使用工廠方法模式來實現的類圖,有沒有感覺這個模式一旦添加了新產品維護起來特別的麻煩,因為不僅要創建新產品的具體實現,而且為了支持它,還必須修改抽象創建者和它的每一個具體實現。

  這種時候應該使用抽象工廠模式,抽象工廠模式是對工廠進行一次抽象。統一對象的獲取接口,使用參數來決定返回的對象。修改下:(ps:感覺就是使用簡單工廠模式對工廠方法模式再進行一次抽象)

abstract class CommsManager
{
    const APPT    = 1;
    const TTD     = 2;
    const CONTACT = 3;
    
    abstract function getA();
    abstract function getB();
    abstract function make($flg);
}

class BloggsCommsManger extends CommsManager
{
    public function getA()
    {
        // TODO: Implement getA() method.
    }

    public function getB()
    {
        // TODO: Implement getB() method.
    }

    public function make($flg)
    {
        switch ($flg) {
            case self::APPT:
                return new BloggsApptEncoder();
            case self::TTD:
                return new BloggsTtdEncoder();
            case self::CONTACT:
                return new BloggsContactEncoder();
            default:
                return new BloggsApptEncoder();
        }
    }
}

技術分享圖片

  可以看到所有創建對象接口統一到 make() 方法,接口變得更加緊湊。但是這樣也暴露出了一些簡單工廠模式的弊端,如開放閉合原則。使用抽象工廠模式會使工廠類更容易維護,基類 CommsManager 可以提供默認的 make() 實現,子類可以實現自己的調用基類的。

  關於工廠方法和抽象工廠的區別

  1. 工廠方法產出的是產品(實例),抽象工廠產出是接口(創建每個實例的接口)
  2. 工廠方法是產出單一對象,抽象工廠產出是一系列相關產品的對象

技術分享圖片

  關於工廠方法和抽象工廠的適用:無論是什麽樣的設計模式,最切合當前業務場景的就是最適用的。個人理解,如果我們的產品是,奔馳、寶馬、五菱宏光之類單一類別產品就比較適用工廠方法模式。但是有一天我們的產品除了汽車還需要添加華碩、聯想、惠普等另一種類別產品的時候就要考慮使用抽象工廠模式了。抽象工廠方法更適用於對產品族的處理。

4. 結語

  終於抽出時間學習設計模式了o(╥﹏╥)o,邊學習邊寫隨筆,寫東西有助於更好理解。好的設計模式運用能使整體代碼結構更清晰,維護擴展性更高。感謝閱讀!再會!

設計模式:對象生成(單例、工廠、抽象工廠)