2021年11月6日

設計模式 | 單例模式 Singleton Pattern 介紹,以 PHP 程式重構為例

大家寫程式的時候,有沒有遇過這樣的情況呢?

有某一種資源,經常被使用,而且每次使用前都要做初始化,比如說資料庫,每次使用前都要設定連線;或是,有些資源如果被重複使用,會造成一些問題,例如同樣以資料庫為例,多次的連線,會影響效能。

如果大家也有這樣的困擾,那本篇將要介紹的單例模式 Singleton Pattern 就是被設計出來解決這個問題的,有興趣的讀者可以繼續往下閱讀哦。

什麼是Singleton Pattern ?它的定義是:

負責進行某一類別的物件創建,並且確保這個類別最多只能有一個物件被創建。

我們先把定義放在心上就好,我們會透過重構範例程式碼來說明這是什麼意思。

範例

NOTE: 本篇範例程式碼皆為 PHP。

我們實作一個簡單的資料庫類別 Class Database

class Database 
{
    protected $host = "0.0.0.0";
    
    public function __construct()
    {
    } 

    public function setHost($host)
    {
        $this->host = $host;
    }

    public function getHost()
    {
        return $this->host;
    }
}

假設我們有兩個類別 HomePageControllerUserPageController 需要使用到資料庫:

class HomePageController
{
    protected $database = null;
    
    public function prepare()
    {
        $this->database = new Database;
        $this->database->setHost('127.0.0.1');
    }
    
    public function get()
    {
        $this->database->get(...);
    }
}

class UserPageController
{
    protected $database = null;
    
    public function prepare()
    {
        $this->database = new Database;
        $this->database->setHost('127.0.0.1');
    }
    
    public function get()
    {
        $this->database->get(...);
    }
}


$homePageController = new HomePageController();
$homePageController->prepare();
$homePageController->get();

$userPageController = new UserPageController();
$userPageController->prepare();
$userPageController->get();

Database 每次使用前都需要進行初始化設定,因此兩個類別分別需要在自己的 prepare 中設定資料庫連線,接著再透過 get 去資料庫拿資料。

隨著使用 Database 的類別越來越多,這樣的設定會到處分散,難以管理,而另一個問題是,程式會會對資料庫做多次連線,進而降低執行效率。

為了改善這樣的問題,我們將使用 Singleton Pattern 進行重構。

使用 Singleton Pattern 重構

我們直接使用 Singleton PatternClass Database 進行重構。

class Database 
{
    protected static $instance = null;
    protected $host = "0.0.0.0";
    
    private function __construct()
    {

    } 

    public function setHost($host)
    {
        $this->host = $host;
    }

    public function getHost()
    {
        return $this->host;
    }

    static public function getInstance()
    {
        if (is_null(self::$instance)) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}

這段程式碼就是重構後的結果,確實看起來跟原本很像,最主要的差異是新增了 getInstance 方法,以及將建構子 __construct 的可見度修改為不對外公開的 private

當建構子變成不公開的時候,外界將沒辦法使用 new 關鍵字來實體化該類別。我們透過這樣的方式,來避免開發者到處實體化 Database。因此執行 new Database; 會得到這樣的錯誤訊息:

PHP Fatal error:  Uncaught Error: Call to private Database::__construct()

如果沒辦法自行實體化,那想要使用 Database 該怎麼辦?別擔心,這就是為什麼我們新增了 getInstance 方法。使用方式像這樣:

Database::getInstance();

接著先解釋一下運作的方式,如果對 PHP static 用法熟悉的人,可以跳過。

稍微說明 Static 用法

getInstance 被宣告為 static 靜態方法,靜態方法屬於類別本身,因此不需要 new 一個物件出來就可以使用,而是透過 Scope Resolution Operator (::) 來使用呼叫靜態方法,而$instance 也被宣告成 static, 代表它是靜態屬性 (static properties) ,同樣也可以透過 :: 來使用它。

因為靜態方法或屬性不需要物件就可以被呼叫,所以在類別內需要用到他們時,不使用 $this,而是使用 self ,在一般情況下,self 代表的是這個類別自己,這邊的話就是Database 這個類別,因此 self::$instance 指的其實是 Database::$instance,同理,new self() 等同於 new Database()

static public function getInstance()
{
	if (is_null(self::$instance)) {
		self::$instance = new self();
	}
	return self::$instance;
}

了解 static 後,可以重新回來看一下 getInstance() 的內容,它會先檢查 self::$instance 是否已經存在,如果不存在,則 new 出一個 Database ,並指派給 self::$instance ,最後再回傳 self::$instance 也就是 Database 物件 (object) 。

Singleton Pattern 使用方式

我們修改了前面範例的 HomePageControllerUserPageController ,移除了 prepare,並修改get方法:

class HomePageController
{
    public function get()
    {
        Database::getInstance()->get(...);
    }
}

class UserPageController
{
    public function get()
    {
        Database::getInstance()->get(...);
    }
}


// 設定
Database::getInstance()->setHost('127.0.0.1');

$homePageController = new HomePageController();
$homePageController->get();

$userPageController = new UserPageController();
$userPageController->get();

靜態方法跟成員都是跟著類別本身,因此不管在哪裡呼叫 Database::getInstance() 拿到的都會是同一個 Database 物件,也就是說,只要設定一次,其他地方就會使用同樣的設定。

在範例中,我們在呼叫 HomePageControllerUserPageController 之前就設定好了 Database,因此它們不需要在自己設定一次,也就不需要 prepare 了,可以在 get 中直接使用。

這麼一來就解決了前面提到的兩個問題,不用到處設定,也不會有重複地連線。

單例模式 Singleton Pattern 討論

複習一下一開始所提到的,Singleton Pattern 的定義:

負責進行某一類別的物件創建,並且確保這個類別最多只能有一個物件被創建。

為了達到這個目的,我們將類別本身的建構子隱藏起來,並提供一個靜態方法供外界使用,由這個靜態方法來處理物件的創立,而這樣的特性,適合用在共享性的資源上,例如範例中的資料庫,或快取伺服器等等。

Singleton Pattern 是反模式 (anti-pattern) 嗎?

實作 Singleton Pattern 後可以透過類別的靜態方法取得物件,但這樣的一個物件,其實幾乎等同於全域變數 (global variables) ,全域變數對於大多數開發者來說都是一場可怕的噩夢,很多時候大家都盡可能地避免使用全域變數,

而 Singleton Pattern 跟全域變數十分像似的特性,讓部分人認為它是個反模式。

所以不該使用 Singleton Pattern 嗎?

我個人認為,Singleton Pattern 利用物件導向的特性來實作,相較於完全赤裸的全域變數而言,沒有那麼脆弱,因此在一些特定的情況中,還是適用的。但是任何模式再怎麼好用都需要控管,尤其是這樣的模式,更要小心不要濫用。

曾遇過一位工程師,他因為不想到處 new 物件,便將所有的類別都套用 Singleton Pattern,因為這樣的理由而使用 Singleton 就不是那麼正確了。


參考

wiki - 單例模式