PHP V5.3 通過其延後靜態繫結(LSB)特性解決了物件導向編程(OOP)的一些問題。瞭解 LSB 如何修複 PHP 的 OOP 編程問題以及如何?需要使用 LSB 的一些眾所周知的物件導向設計模式。
物件導向編程(OOP)可讓開發人員通過使用資料抽象、封裝、模組化、多態性和繼承減少和簡化代碼 — 在對 OOP 有著深刻的理解的前提下。對 OOP 特性的瞭解還讓 PHP 編碼者得以利用設計模式 — 一些眾所周知的用來解決常見問題的演算法。PHP 自 V3.0 就已經提供了 OOP 功能,但直到 V5.3 到來時,PHP 的 OOP 實現內的怪異之處還是會阻止一些常見設計模式的使用。隨著 PHP V5.3 的延後靜態繫結(LSB)特性的出現,這些怪異之處均已徹底消失。
本文向您介紹了在 PHP V5.3 出現之前,存在問題的一些設計模式,解釋了這些模式為何不能工作。然後展示了 PHP V5.3 的 LSB 特性,並給出了單例和活動記錄設計模式。
重新回顧 OOP
如果您過去曾接觸過 PHP OOP,那麼很可能會出於以下原因而決定不使用它:
讀過諸多宣稱 PHP OOP 有問題的博文中的一條。
曾嘗試實現一個簡單的設計模式,但沒有成功。
而對於 PHP V5.3,有關 OOP 的博文都是正面的,並且 PHP OOP 的問題在很大程度上已得到解決。是時候重回 PHP OOP 了。通過本文,您將看到在 V5.3 出現之前曾存在問題的一些設計模式:單例、產生器、Factory 方法和活動記錄。
單例、產生器和Factory 方法設計模式被視為是 建立型 的模式,因它們可協助對象的構建。單例模式可能是最常用的 OOP 設計模式之一了 ;它限制了一個類的對象執行個體數只能為 1。比如資料庫連接池就是單例設計模式的一個例子:我們一般不想讓應用程式具有串連池類的多個資源密集型執行個體。
在需要分離複雜物件的構建和表示時,就需要用到產生器設計模式,您可以使用相同的構造過程來建立多個對象。產生器模式的實現可以很複雜,但一旦產生器可用,它就可以簡化產生器所建立對象的構造和使用。具有輸出 HTML、XML 或 PDF 能力的轉變器就是需要使用產生器的一個例子。
而Factory 方法模式,顧名思義,定義的是一個用來大量產出對象的方法的實現。您可以在應用程式需要建立其類型依賴於子類的實現的對象時,使用Factory 方法模式。
活動記錄模式則可用來在域類內封裝關聯式資料庫持久性方法。一個活動記錄的每個執行個體都關係到資料庫內的特定行。這個類包含了要插入、刪除和更新資料庫內的一行或多個行的方法。活動記錄設計模式是由 Martin Fowler 在 Patterns of Enterprise Application Architecture 內定義的,並因在 Ruby on Rails 內的使用而日益流行。
前-LSB 的建立型設計模式實現問題
上述提到的所有這四個設計模式均使用了靜態屬性和方法。例如,看一下清單 1 內所示的這個串連池單例。
清單 1. 一個簡單的單例
<?php
class ConnPool {
private static $onlyOne;
private static $count = 0;
private function __construct() {
// real-world db conn stuff here...
}
public static function getInstance() {
if (!is_object(self::$onlyOne)) {
$klass = __CLASS__;
self::$onlyOne = new $klass();
self::$count++;
}
return self::$onlyOne;
}
public static function getInstanceCount() {return self::$count;}
}
$db = ConnPool::getInstance();
assert (1 == $db->getInstanceCount());
$db2 = ConnPool::getInstance();
assert (1 == $db2->getInstanceCount());
?>
請注意這個靜態 $onlyOne 變數。該變數被設計用來儲存串連池對象的一個執行個體。$onlyOne 之前的靜態修飾符將此變數關係到類本身。$onlyOne 變數是一個類屬性,因為其範圍是這個類。而 $onlyOne 屬性只有一個執行個體。當一個屬性不具有靜態修飾符時,就稱其是一個對象屬性,因為該屬性對類的每個執行個體都是惟一的。
注意到 ConnPool 的建構函式方法(called __construct)是空的。在一個生產實現中,可以使用該方法來建立資料庫連接池的間隔。
靜態 getInstance 方法包含單例的模板代碼。只有在靜態 $onlyOne 變數為空白時,它才會建立一個 $onlyOne 執行個體。請注意它是如何使用 __CLASS__ 變數來獲得類的類型並隨即建立該類的一個執行個體的。
使用 getInstanceCount 方法只是為了證明只建立了串連池的一個執行個體。清單 1 底部的四行代碼則證明無論請求 ConnPool 池類的一個執行個體多少次,它都會返回相同的對象。
所以, 到目前為止,此單例一切正常 — 直到您決定想要以物件導向的繼承樹的形式對這個串連池進行子類處理來支援多個資料庫。清單 2 顯示了這個繼承樹(為了清晰起見,刪除了執行個體計數器和建構函式代碼)。
清單 2. 在沒有 LSB 時對單例進行的一次失敗嘗試
<?php
class ConnPool {
private static $onlyOne;
protected static $klass = __CLASS__;
public static function getInstance() {
if (!is_object(self::$onlyOne)) {
self::$onlyOne = new self::$klass();
}
return self::$onlyOne;
}
}
class ConnPoolAS400 extends ConnPool {
protected static $klass = __CLASS__;
}
$db = ConnPoolAS400::getInstance();
assert ('ConnPoolAS400' == get_class($db)); // fails
?>
為了支援多個類型的單例類,ConnPool 類添加了一個 $klass 靜態變數並假設它會在子類中被覆蓋。 ConnPoolAS400 子類擴充了 ConnPool 類並提供了 $klass 屬性自己的版本。 我們的預期是當 ConnPoolAS400 類的執行個體建立時,$klass 屬性會儲存 ConnPoolAS400。但是當執行這些代碼時,它不會按預期的那樣運行。當 PHP 實用函數 get_class 返回 ConnPool 而不是 ConnPoolAS400 時,代碼底部的聲明會失敗。 問題是 ConnPool 類的 getInstance 方法使用的是它自己的 $klass 屬性版而非 ConnPoolAS400 的覆蓋版。