大家寫程式的時候,有沒有遇過這樣的情況呢?
有某一種資源,經常被使用,而且每次使用前都要做初始化,比如說資料庫,每次使用前都要設定連線;或是,有些資源如果被重複使用,會造成一些問題,例如同樣以資料庫為例,多次的連線,會影響效能。
如果大家也有這樣的困擾,那本篇將要介紹的單例模式 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;
}
}
假設我們有兩個類別 HomePageController
及 UserPageController
需要使用到資料庫:
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 Pattern
將 Class 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 使用方式
我們修改了前面範例的 HomePageController
及 UserPageController
,移除了 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
物件,也就是說,只要設定一次,其他地方就會使用同樣的設定。
在範例中,我們在呼叫 HomePageController
及 UserPageController
之前就設定好了 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 就不是那麼正確了。
參考