在說明單例模式之前,我們先來看看一個例子。

假設我們有一個命名為 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 類別實例化出來的兩個物件,是不同的物件。

單例模式

顧名思義,就是限制類別只能實例化出一個物件。

(1) 限制建構子

我們必須把它的建構子 (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

(2) 加入靜態屬性

我們新增了一個名為 instance 的靜態屬性。

範例:

class Man
{
    /**
     * @param null|object
     */
    private static $instance;

    /**
     * @param int
     */
    public $age = 0;

    /**
     * Constructor
     */
    private function __construct()
    {

    }

    // 以下省略

該屬性設為 private 以限制只能使用公開的存取方法。

(3) 加入公開的存取方法

由於 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 介紹的設計模式概念,以武林高手的招式來比喻,武林高手的招式總是視時機穿插使用的,當然,單例配合其它設計模式混合使用也很很常見的。

接下來幾天就會介紹這樣的例子,實際上運用的例子會讓大家更清楚整個概念。

我們明天見。

最後修改日期: 2022-01-31

作者

留言

撰寫回覆或留言

發佈留言必須填寫的電子郵件地址不會公開。