註冊表模式 (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 資料庫連接都註冊起來。
先在專案中實例化 Redis
及 PDO
類別。
$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),歡迎前往討論。
這邊寫錯了
throw new Expection('此物件尚未被註冊。');
是
throw new Exception('此物件尚未被註冊。');
謝謝提醒,已修正