幾乎所有面向對象的程序中,總有一兩個資源被創建出來,在程序應用中持續被共享使用。例如,這樣的一個資源,在一個電子商務程序的數據庫連接中使用:這個連接在應用程序啟動時初始化,程序於是可以有效的執行;當程序結束時,這個連接最終被斷開並銷毀。如果是你寫的代碼,沒必要在每時每刻創建一個數據庫連接,這樣非常低效。已經建立好的連接應該能被你的代碼簡單重復的使用。這個問題就是,基於以上要求你將如何進行這個數據庫連接?(或者連接其它被循環使用的唯一資源,比如一個開放文件或者一個隊列。)
問題
你怎樣確保一個特殊類的實例是獨一無二的(它是這個類的唯一實例),並且它很存取容易呢?
解決方案
當然,全局變量是顯而易見的解決方案。但它就像潘多拉的盒子(正確的判斷來自經驗,而錯誤的判斷產生經驗。這句諺語就是這個意思。),你的任何代碼都能修改全局變量,這將不可避免的引起更多調試的意外。換句話說,全局變量的狀態總是會出現一些問題的,(這裡有一個關於全局變量使用問題不錯的描述,http://c2.com/cgi/wiki?GlobalVariablesAreBad)。
當你需要一個特殊類的唯一實例時,使用這個名字叫單件的模式。基於單件模式的類能實例化和初始化這個類的一個實例,並且提供每時每刻絕對相同的連接。一般情況下使用名為getInstance()的靜態方法實現。
關鍵問題是,如何在每時每刻獲得一個精確統一的實例。請看下面的例子:
// PHP4
function TestGetInstance() {
$this->assertIsA(
$obj1 =& DbConn::getInstance(),
‘DbConn’,
‘The returned object is an instance of DbConn’);
$this->assertReference(
$obj1,
$obj2 =& DbConn::getInstance(),
‘Two calls to getInstance() return the same object’);
}
注釋:assertReference
assertReference() 方法確保兩個被傳遞的參數引用自相同的PHP變量。
在PHP4中,這裡斷言兩個被測試的參數的卻是相同的對象。assertReference() 這個方法在移植到PHP5以後也許就不推薦使用了。
這個test方法有兩個斷言:第一個判斷第調用靜態方法DbConn::getInstance()返回的值是DbConn對象的實例,第二個用來判斷第二次調用getInstance()方法返回得值引用的是相同的對象實例,這意味著他們使用的是同一個對象。
除了斷言代碼預期的執行結果,Test也預示了getInstance()的正確用法(PHP4):$local_conn_var=&DbConn::getInstance()。引用(=&)靜態方法的返回值賦值給了這個局部變量。
再寫另外一段測試代碼:直接用“new”來實例化一個單件類會引起某些類型的錯誤。test代碼如下:
function TestBadInstantiate() {
$obj =& new DbConn;
$this->assertErrorPattern(
‘/(bad|nasty|evil|do not|don’t|warn).*’.
‘(instance|create|new|direct)/i’);
}
這段代碼直接創建了一個 DbConn 的實例,將會引起PHP報錯。為了讓代碼更穩定,我們用PCRE正則表達式來匹配報錯信息。(顯示報錯信息的確切措詞並不重要。)
[next]
樣本代碼
單件模式是一個很有趣的模式。讓我們用PHP4和PHP5兩種方式來探究它的實現過程,現在從PHP4開始。
全局方式
理論上說,一個全局變量可以生成一個完美的單件,但全局變量可能被修改:在代碼運行過程中,不能保證全局變量指向的是一個對象。因而,不讓全局變量在全局直接引用,就可以減少“太隨意訪問”這個全局變量的問題。比如說,這段代碼使用一個非常長而且獨特的名字,從而“隱藏” 了全局變量的引用。
class DbConn {
function DbConn($fromGetInstance=false) {
if (M_E != $fromGetInstance) {
trigger_error(‘The DbConn class is a Singleton,’
.’ please do not instantiate directly.’);
}
}
function &getInstance() {
$key = ‘__some_unique_key_for_the_DbConn_instance__’;
if (!(array_key_exists($key, $GLOBALS) && is_object($GLOBALS[$key])
&& ‘dbconn’ == get_class($GLOBALS[$key]) )) {
$GLOBALS[$key] =& new DbConn(M_E);
}
return $GLOBALS[$key];
}
}
在DbConn的構造函數中,你可能對$fromGetInstance的默認參數感到疑惑。在對象被直接實例化時,它能夠提供(很微弱的)保護:除非這個默認值變成e (在PHP的數學常量中 M_E = 2.718281828459),否則這段代碼會報錯。
表示成一個UML類圖,解決辦法如下:
如果你不選用這個“神秘參數”-類型保護,建立一個全局標記是另外一個選擇,用它來驗證你是通過getInstance()方法來創建的對象。保護方式從“你知道它的名字”改變成“它存在於環境中”。
下面有個例子,它解釋了為什麼構造函數保護代碼有一個全局的標識:
class DbConn {
function DbConn() {
$token = ‘__some_DbConn_instance_create_semaphore__’;
if (!array_key_exists($token, $GLOBALS)) {
trigger_error(‘The DbConn class is a Singleton,’
.’ please do not instantiate directly.’);
}
}
function &getInstance() {
static $instance = array();
if (!$instance) {
$token = ‘__some_DbConn_instance_create_semaphore__’;
$GLOBALS[$token] = true;
$instance[0] =& new DbConn;
unset($GLOBALS[$token]);
}
提示
PHP4允許你改變構造函數中$this的值。在過去,我們會習慣設置 $this = null;當有一個創建構造錯誤時,確保無效的對象不能被代碼繼續使用。PHP4中很有用的東西,在PHP5中並不兼容,將來會在你的代碼中得到驗證,這種技術不再被推薦。
這段代碼中另外一個重點是引用操作&的用法。有兩種地方需要使用&。第一種是在函數定義時,在函數名字前用來表示將返回一個引用。第二種是將新的DbConn對象賦值給$GLOBALS數組。(在序言和值對象章節中提到過:在PHP4中,你總會使用 &操作符,以引用的方式創建、傳遞和返回對象,)
getInstance()方法的條件檢查,常常被寫成沒有警示的情況下運行,甚至在E_ALL的錯誤級別下也不會提示。它檢查在$GLOBAL數組中適當的位置是否有一個DbConn對象,如果沒有,就在那裡創建這個對象。這個方法於是返回了這樣的結果,這個對象能被重復創建或者這個對象在之前已經被這個方法創建過了。當方法結束時,你可以確認已經擁有這個類的有效實例,而且它已經被有效初始化。
[next]
靜態方式
關於全局變量的問題,甚至隱藏在getInstance()中的全局變量中也存在。因為全局變量在腳本的任何地方都有效,在沒有注意到的情況下,你依然有可能破壞這個全局變量,
在getInstance()方法內部使用靜態變量來存儲Singleton是一個顯得干淨的辦法。第一個代碼片斷如下:
class DbConn {
// ...
function &getInstance() {
static $instance = false;
if (!$instance) $instance =& new DbConn(M_E);
return $instance;
}
}
Zend 1引擎在PHP4中不能存儲靜態變量的引用 (請看http://www.php.net/manual/en/language.variables.scope.php#AEN3609)。使用一個工作區存儲靜態數組,並且將這個單件實例的引用放置到一個已知的數組中。getInstance()方法如下:
class DbConn {
function DbConn($fromGetInstance=false) {
if (M_E != $fromGetInstance) {
trigger_error(‘The DbConn class is a Singleton,’
.’ please do not instantiate directly.’);
}
}
function &getInstance() {
static $instance = array();
if (!$instance) $instance0 =& new DbConn(M_E);
return $instance0;
}
}
這段代碼很簡單的選擇了這個靜態數組$instancede的第一個元素,用來保持單件DbConns實例的引用。
雖然這段代碼有點依賴PHP的布爾方式,但它比那個全局版本更嚴謹:在條件檢測時,使用一個空的數組會得到結果false。就像在DbConn類的前一個版本一樣,在函數的定義和賦值部分需要引用操作符。
PHP5中的單件模式
PHP5中更容易實現單件模式,PHP5對於類內部變量和函數的訪問控制被加強了。將DbConn::_construct()構造方法設置為私有(priva