在說明單例模式之前,我們先來看看一個例子。
假設我們有一個命名為 Man
的類別 (class) ,帶有一個名為 age
的屬性 (property),和三個方法 (method),分別為 sayHello
、setAge
、及 getAge
。
class Man
{
/**
* @param int
*/
public $age = 0;
/**
* 印出字串 Hello
*
* @return void
*/
public function sayHello()
{
echo 'Hello' . "\n";
}
/**
* 設定年紀
*
* @param int $age 年紀
*
* @return void
*/
public function setAge($age)
{
$this->age = $age;
}
/**
* 印出年紀
*
* @return void
*/
public function getAge()
{
echo $this->age . "\n";
}
}
Man
這個類別,我們可以實例化好幾個出來使用,如下,變數 $joe
及 $bill
為實例化 Man
類別的物件。
$joe = new Man();
$bill = new Man();
$joe->sayHello();
$bill->sayHello();
$joe->setAge(23);
$bill->setAge(40);
$joe->getAge();
$bill->getAge();
$joe->getAge();
結果:
Hello
Hello
23
40
23
使用 Man
類別實例化出來的兩個物件,是不同的物件。
顧名思義,就是限制類別只能實例化出一個物件。
我們必須把它的建構子 (constructor) 改成私有 (private),如此一來便不能藉由 new
關鍵字去實例化它。
class Man
{
/**
* @param int
*/
public $age = 0;
/**
* Constructor
*/
private function __construct()
{
}
// 以下省略
將建構子的存取類型改為 private
之後,試著實例化它:
$joe = new Man();
則會出現錯誤的警告訊息。
Fatal error: Uncaught Error: Call to private Man::__construct() from invalid context in [...][...]:51
Stack trace:
#0 {main}
thrown in [...][...]on line 51
我們新增了一個名為 instance
的靜態屬性。
class Man
{
/**
* @param null|object
*/
private static $instance;
/**
* @param int
*/
public $age = 0;
/**
* Constructor
*/
private function __construct()
{
}
// 以下省略
該屬性設為 private
以限制只能使用公開的存取方法。
由於 Man 類別已無法從外部實例化,因此只能使用靜態的方法去存取它。
我們新增了一個名為 getInstance
的靜態方法。
class Man
{
/**
* @param null|object
*/
private static $instance;
/**
* @param int
*/
public $age = 0;
/**
* Constructor
*/
private function __construct()
{
}
/**
* 取得實例
*
* @return self
*/
public static function getInstance()
{
if (!self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
// 以下省略
當 self::$instance
為 null
時,則會在類別內部實例化自己,然後存進 self::$instance
,往後就直接從 $instance
這個靜態屬性取得該類別的實例。
如此一來只會有唯一的物件被實例化出來。
註:getInstance
是很普遍的命名,在不同程式語言皆會看到以此命名的方法來使用單例。以此命名能增加可讀性。
我們從一開始的例子,把 new
關鍵字拿掉,改調用 getInstance
這個靜態方法。
$joe = Man::getInstance();
$bill = Man::getInstance();
$joe->sayHello();
$bill->sayHello();
$joe->setAge(23);
$bill->setAge(40);
$joe->getAge();
$bill->getAge();
$joe->getAge();
結果
Hello
Hello
40
40
40
在整個 PHP 專案中需要一個全域的物件,但不希望創造該物件的類別能再實例出另一個相同的物件時。例如資料庫連接 (database connection)、容器 (container)、事件處理 (event) 等等。
還記得在 Day 2 筆者為大家整理的 PHP 版本差異說明,提到在 PHP 5.4 版的新功能,特性 (trait) 嗎?我們可以建立一個單例特性,讓使用該特性的類別直接套用單例模式。
trait Singleton
{
/**
* @param null|object
*/
private static $instance;
/**
* Constructor
*/
private function __construct()
{
}
/**
* 取得實例
*
* @return self
*/
public static function getInstance()
{
if (!self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
}
還記得本文一開場就給的範例程式碼嗎?
class Man
{
use Singleton;
/**
* @param int
*/
public $age = 0;
// 以下省略...
我們在 Man
類別裡直接使用 Singleton
特性,讓 Man
類別套用單例模式,套用只能使用 getInstance
靜態方法來創造物件。
$man = Man::getInstance();
$man->sayHello();
結果:
Hello
用 new
關鍵字試試:
$man = new Man();
$man->sayHello();
結果:
Fatal error: Uncaught Error: Call to private Man::__construct() from invalid context in [...][...]:77
Stack trace:
#0 {main}
thrown in [...][...]on line 77
使用 Singleton
這個特性之後已無法透過 new
去實例化物件了。
就如筆者在 Day 3 介紹的設計模式概念,以武林高手的招式來比喻,武林高手的招式總是視時機穿插使用的,當然,單例配合其它設計模式混合使用也很很常見的。
接下來幾天就會介紹這樣的例子,實際上運用的例子會讓大家更清楚整個概念。
我們明天見。
本文同步更新於 TerryL 部落格 Day 4 - PHP 設計模式:單例 (Singleton),歡迎前往討論。