註冊表模式 (registry pattern) 是一個作用域為全域性 (global scope) 的類別,本身不負責創造物件,而是儲存其它物件以便重複使用。一旦有需要使用到該物件時,再從註冊表中提取調用。

定義註冊表類別

典型的註冊表會有一個靜態 (static) 的屬性、四個公開 (public) 的方法用來存放其它類別實例化的物件。

這四個方法分別為 set, get, has, remove

set

儲放物件至註冊表中。

/**
 * @param object $obj  要存入的物件實例
 * @param string $name 代表此物件的名稱
 * @return void
 */
public static function set($obj, $name = '')
{
    // 假如沒有指定名稱,則使用該物件類別的名稱。
    if (empty($name)) {
        // PHP 函式說明
        // get_class:  取得類別的名稱。
        // strtolower: 將字串小寫化。
        $name = strtolower(get_class($obj));
    }

    if (self::has($name)) {
        // 假如該名稱代表的物件已註冊,則略過之後的動作。
        return;
    }
    self::$instances[$name] = $obj;
}

get

從註冊表中取得物件。

/**
 * @param string $name 物件的名稱
 * @return mixed
 */
public static function get($name)
{
    if (!self::has($name)) {
        throw new Exception('此物件尚未被註冊。');
    }

    return self::$instances[$name];
}

has

查詢註冊表中是否有特定名稱的物件。

/**
 * @param string $name 物件的名稱
 * @return mixed
 */
public static function has($name)
{
    if (isset(self::$instances[$name])) {
        return true;
    }
    return false;
}

remove

從註冊表中移除特定名稱的物件。

/**
 * @param string $name 物件的名稱
 * @return void
 */
public static function remove($name)
{
    if (self::has($name)) {
        unset(self::$instances[$name]);
    }
}

Registry 範例:

class Registry
{
    /**
     * 存放物件的容器。
     *
     * @param array
     */
    private static $instances = [];

    /**
     * @param string $name 物件的名稱
     * @return mixed
     */
    public static function get($name)
    {
        if (!self::has($name)) {
            throw new Exception('此物件尚未被註冊。');
        }

        return self::$instances[$name];
    }

    /**
     * @param object $obj  要存入的物件實例
     * @param string $name 代表此物件的名稱
     * @return void
     */
    public static function set($obj, $name = '')
    {
        // 假如沒有指定名稱,則使用該物件類別的名稱。
        if (empty($name)) {
            // PHP 函式說明
            // get_class:  取得類別的名稱。
            // strtolower: 將字串小寫化。
            $name = strtolower(get_class($obj));
        }

        if (self::has($name)) {
            // 假如該名稱代表的物件已註冊,則略過之後的動作。
            return;
        }

        self::$instances[$name] = $obj;
    }

    /**
     * @param string $name 物件的名稱
     * @return mixed
     */
    public static function has($name)
    {
        if (isset(self::$instances[$name])) {
            return true;
        }
        return false;
    }

    /**
     * @param string $name 物件的名稱
     * @return void
     */
    public static function remove($name)
    {
        if (self::has($name)) {
            unset(self::$instances[$name]);
        }
    }
}

應用情境

以下是一個應用情境,我們把專案中會用的 MySQL 資料庫連接,以及 Redis 資料庫連接都註冊起來。

先在專案中實例化 RedisPDO 類別。


$redisHost = '127.0.0.1';
$redisPort = 6379;

// 實例化 Redis 類別,以連接 MySQL 資料庫
// Redis 類別為 PHP 擴充套件 php_redis 提供,必須系統上安裝才能使用。
$redis = new Redis();
$redis->connect($redisHost, $redisPort);

 try {
    $redis = new Redis();
    $redis->connect( '127.0.0.1', 6379 );
} catch(RedisException $e ) {
    echo 'Redis Connection failed: ' . $e->getMessage();
}

// 實例化 PDO 類別,以連接 MySQL 資料庫

$mysqlDsn = 'mysql:dbname=testdb;host=127.0.0.1';
$mysqlUser = 'dbuser';
$mysqlPass = 'dbpass';

try {
    $pdo = new PDO($mysqlDsn, $mysqlUser, $mysqlPass);
} catch (PDOException $e) {
    echo 'MySQL Connection failed: ' . $e->getMessage();
}

註冊表模式的應用中,不一定是要靜態的方法,但靜態方法的好處是它可以讓註冊表直接使用,而不用先行實例化註冊表,可以直接呼叫。

以下將之前已經實例化的 $pdo$redis 註冊進來吧。

Registry::set($redis, 'redis');
Registry::set($pdo, 'db');

然後我們就可以在專案中任何一個需要使用 MySQL 及 Redis 的地方從註冊表中拉出來用囉!

取得 Redis 實例:

$redis = Registry::get('redis');

取得 PDO 實例:

$pdo = Registry::get('db');

取消 Redis 的註冊:

$redis = Registry::remove('redis');

別名

註冊表模式在每一個先進框架中,都會有它的影子。筆者在先前的文章有提到,設計模式指的是一種概念、一種習慣模式,但依每個人的實作細節會有所差異,因此它的名字及調用方法有些差別,可能會是 Registry,也可能是 Container,也可能會被命名為 Service,不一定,端看作者的主觀喜好。

不同的命名,用的是同樣的設計模式。用命名為 Container(容器)是蠻常見的。

範例:

class Container
{
    /**
     * 存放物件的容器。
     *
     * @param array
     */
    private static $instances = [];

    /**
     * 從容器中取得物件。
     *
     * @param string $name 物件的名稱
     *
     * @return mixed
     */
    public function get($name)
    {
        if (!self::has($name)) {
            throw new Exception('此物件尚未被註冊。');
        }

        return self::$instances[$name];
    }

    /**
     * 儲放物件至容器中。
     * 
     * @param object $obj  要存入的物件實例
     * @param string $name 代表此物件的名稱
     *
     * @return void
     */
    public function set($obj, $name = '')
    {
        if (empty($name)) {
            $name = strtolower(get_class($obj));
        }

        if (self::has($name)) {
            return;
        }

        self::$instances[$name] = $obj;
    }

    /**
     * 查詢容器中是否有特定名稱的物件。
     *
     * @param string $name 物件的名稱
     *
     * @return mixed
     */
    public function has($name)
    {
        if (isset(self::$instances[$name])) {
            return true;
        }
        return false;
    }

    /**
     * 從容器中移除特定名稱的物件。
     *
     * @param string $name 物件的名稱
     *
     * @return void
     */
    public function remove($name)
    {
        if (self::has($name)) {
            unset(self::$instances[$name]);
        }
    }
}

這個例子所使用的 Container 類別用的方法並非靜態方法,因此使用它必須先實例化 Container

$container = new Container();

$container 在專案中是一個全域的變數,或,在物件導向為主的框架中, $this->container 屬性來存放及取用註冊進來的物件。筆者的範例都是假設它為一個全域變數的情況。

以下將之前已經實例化的 $pdo$redis 註冊進來吧。

$container->set($redis, 'redis');
$container->set($pdo, 'db');

取得 Redis 實例:

$redis = $container->get('redis');

取得 PDO 實例:

$pdo = $container->get('db');

總結

不論是類別名稱的不同,或類別裡使用的方法名稱、是否為靜態或非靜態的方法,用來存放及取用已實例化的物件,就是一種註冊表模式。甚至註冊閉包函式 (closure)、設定值用的陣列,也很常見。

假如要很嚴格地限制只能註冊物件,那麼只要限制回傳值的類型即可。

明天,筆者會介紹單例模式和註冊表模式的混合用法。我們明天見囉。

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

作者

留言

撰寫回覆或留言

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