iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 5
1
Software Development

PHP 大師之路 - 開源的技術淬練系列 第 5

Day 5 - PHP 設計模式:註冊表 (Registry)

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

定義註冊表類別

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

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

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)、設定值用的陣列,也很常見。

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

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


本文同步更新於 TerryL 部落格 Day 5 - PHP 設計模式:註冊表 (Registry),歡迎前往討論。


上一篇
Day 4 - PHP 設計模式:單例 (Singleton)
下一篇
Day 6 - PHP 設計模式:註冊表 (Registry) + 單例 (Singleton)
系列文
PHP 大師之路 - 開源的技術淬練30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
piece601
iT邦新手 5 級 ‧ 2021-07-03 16:29:13

這邊寫錯了

throw new Expection('此物件尚未被註冊。');

throw new Exception('此物件尚未被註冊。');

Terry L. iT邦研究生 4 級 ‧ 2022-01-31 23:54:59 檢舉

謝謝提醒,已修正

我要留言

立即登入留言