程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> PHP綜合 >> PHP V5.3 用延後靜態綁定(LSB)搞活面向對象(OOP)編程

PHP V5.3 用延後靜態綁定(LSB)搞活面向對象(OOP)編程

編輯:PHP綜合

PHP V5.3 通過其延後靜態綁定特性解決了面向對象編程的一些問題。了解 LSB 如何修復 PHP 的 面向對象編程問題以及如何實現需要使用 LSB 的一些眾所周知的面向對象設計模式。
面向對象編程(面向對象)可讓開發人員通過使用數據抽象、封裝、模塊化、多態性和繼承減少和簡化代碼 — 在對 面向對象 有著深刻的理解的前提下。對 面向對象 特性的了解還讓 PHP 編碼者得以利用設計模式 — 一些眾所周知的用來解決常見問題的算法。PHP 自 V3.0 就已經提供了 面向對象 功能,但直到 V5.3 到來時,PHP 的 面向對象 實現內的怪異之處還是會阻止一些常見設計模式的使用。隨著 PHP V5.3 的延後靜態綁定(LSB)特性的出現,這些怪異之處均已徹底消失。
本文向您介紹了在 PHP V5.3 出現之前,存在問題的一些設計模式,解釋了這些模式為何不能工作。然後展示了 PHP V5.3 的 LSB 特性,並給出了單例和活動記錄設計模式。

重新回顧 面向對象

如果您過去曾接觸過 PHP 面向對象,那麼很可能會出於以下原因而決定不使用它:

讀過諸多宣稱 PHP 面向對象 有問題的博文中的一條。

曾嘗試實現一個簡單的設計模式,但沒有成功。

而對於 PHP V5.3,有關 面向對象 的博文都是正面的,並且 PHP 面向對象 的問題在很大程度上已得到解決。是時候重回 PHP 面向對象 了。通過本文,您將看到在 V5.3 出現之前曾存在問題的一些設計模式:單例、生成器、工廠方法和活動記錄。

單例、生成器和工廠方法設計模式被視為是 創建型 的模式,因它們可協助對象的構建。單例模式可能是最常用的 面向對象 設計模式之一了 ;它限制了一個類的對象實例數只能為 1。比如數據庫連接池就是單例設計模式的一個例子:我們一般不想讓應用程序具有連接池類的多個資源密集型實例。

在需要分離復雜對象的構建和表示時,就需要用到生成器設計模式,您可以使用相同的構造過程來創建多個對象。生成器模式的實現可以很復雜,但一旦生成器可用,它就可以簡化生成器所創建對象的構造和使用。具有輸出 HtmlXMLPDF 能力的轉變器就是需要使用生成器的一個例子。

而工廠方法模式,顧名思義,定義的是一個用來大量產出對象的方法的實現。您可以在應用程序需要創建其類型依賴於子類的實現的對象時,使用工廠方法模式。

活動記錄模式則可用來在域類內包裝關系數據庫持久性方法。一個活動記錄的每個實例都關系到數據庫內的特定行。這個類包含了要插入、刪除和更新數據庫內的一行或多個行的方法。活動記錄設計模式是由 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 的覆蓋版。

 清單 2 內的代碼存在的問題是 self 關鍵字綁定到了在編譯時引用的屬性或方法。self 關鍵字指向的是包含類,且不會意識到子類。基本上,編譯器會用所包含類的名稱替換 self 關鍵字。這就類似於如下這行代碼:

self::$onlyOne = new self::$klass();

被編譯器替代為:

ConnPool::$onlyOne = new ConnPool::$klass();

而這就是所謂的提前綁定。而您所需要的是 延後綁定。

有了 LSB 的單例繼承

您可以通過使用 PHP V5.3 的 LSB 功能修復這個單例功能。可以用 static 替換 self 指定符 self::$onlyOne = new self::$klass();:

self::$onlyOne = new static::$klass();

代碼的重新運行的結果是一個成功的聲明。

static 關鍵字會在可能的最近時刻強迫 PHP 綁定到代碼實現。沒有 LSB, self::$klass 會引用所找到的第一塊代碼:父類的版本。

PHP V5.3 內一個名為 get_called_class 的新功能稍稍簡化了單例的代碼。清單 3 用 get_called_class 函數替換了靜態 $klass 屬性的使用。

清單 3. 用 get_called_class 簡化的單例

<?PHP
class ConnPool {
private static $instance;
public function get_instance() {
if (!is_object(self::$instance)) {
$klass = get_called_class();
self::$instance = new $klass();
}
return self::$instance;
}
}

class ConnPoolAS400 extends ConnPool {}
$db = ConnPoolAS400::get_instance();
assert ('ConnPoolAS400' == get_class($db));
?>
清單 3 內的單例顯然更為准確,但更為重要的一點是它使用了 PHP 的 LSB 來引用適當的覆蓋靜態類。盡管單例實現使用的是子類的類名,其他的模式(比如稍後介紹的活動記錄模式)需要引用其他的靜態屬性。此外,LSB 可同時使用靜態函數 和靜態屬性。靜態函數與靜態屬性一樣,作用域也是類而非該類的對象實例。清單 4 顯示了使用方法而非屬性來指定適當類的單例。

清單 4. 在方法上使用了 LSB 的一個單例

<?PHP
class ConnPool {
private static $onlyOne;
protected static function getClass() {
return __CLASS__;
}

public function get_instance() {
if (!is_object(self::$onlyOne)) {
$klass = static::getClass();
self::$onlyOne = new $klass();
}
return self::$onlyOne;
}
}

class ConnPoolAS400 extends ConnPool {
protected static function getClass() {
return __CLASS__;
}
}
$db = ConnPoolAS400::get_instance();
assert ('ConnPoolAS400' == get_class($db));
?>
在此代碼中,靜態 getClass 實現在 ConnPool 內定義並在 ConnPool400 內覆蓋。 ConnPool 的 get_instance 方法的如下代碼行會在運行時調用適當的方法:

$klass = static::getClass();

活動記錄

讓我們先來看看活動記錄設計模式的一個簡單的部分實現。清單 5 顯示了一個名為 ActiveRecord 的抽象類以及兩個子類:Customer 和 Sales。子類是域類,因為它們向存在於應用程序域內的實用工具提供了包裝程序。

清單 5. 活動記錄設計模式的簡單實現

<?PHP
abstract class ActiveRecord {
protected static $table;
protected $fIEldvalues;
public $select; // used for illustration only

static function findById($id) {
$query = "select * from "
.static::$table
." where id=$id";
return self::createDomain($query);
}
function __get($fIEldname) {
return $this->fieldvalues[$fIEldname];
}
static function __callStatic($method, $args) {
$fIEld = preg_replace('/^findBy(\w*)$/', '${1}', $method);
$query = "select * from "
.static::$table
." where $fIEld='$args[0]'";
return self::createDomain($query);
}
// TODO: code a __set method
private static function createDomain($query) {
$klass = get_called_class();
$domain = new $klass();
$domain->fIEldvalues = array();
$domain->select = $query;
foreach($klass::$fields as $fIEld => $type) {
$domain->fieldvalues[$fIEld] = 'TODO: set from sql result';
}
return $domain;
}
// TODO: code static create, update, delete methods
}
class Customer extends ActiveRecord {
protected static $table = 'custdb';
protected static $fIElds = array(
'id' => 'int',
'email' => 'varchar',
'lastname' => 'varchar'
);
}
class Sales extends ActiveRecord {
protected static $table = 'salesdb';
protected static $fIElds = array(
'id' => 'int',
'item' => 'varchar',
'qty' => 'int'
);
}

assert ("select * from custdb where id=123" ==
Customer::findById(123)->select);
assert ("TODO: set from sql result" ==
Customer::findById(123)->email);
assert ("select * from salesdb where id=321" ==
Sales::findById(321)->select);
assert ("select * from custdb where Lastname='Denoncourt'" ==
Customer::findByLastname('Denoncourt')->select);
?>
ActiveRecord 類使用 abstract 修飾符來確保代碼不會實例化一個 ActiveRecord 對象。如果用 new ActiveRecord(); 嘗試創建一個 ActiveRecord,將會收到一個錯誤,稱 “PHP Fatal error: Cannot instantiate abstract class ActiveRecord”。這是一件好事,因為在沒有子類時,ActiveRecord 類不會做任何有價值的事情。

ActiveRecord 類定義一個靜態的 $table 變量,它會相繼被 Customer 和 Sales 子類覆蓋來指定 SQL 表名 custdb 和 salesdb。

ActiveRecord 的靜態 findById 函數是活動記錄設計模式的實現內常見的一個方法的例子。findById 負責基於所傳遞的惟一標識符來檢索數據庫內的適當行,然後再構建並返回代表業務實體的域對象。 findById 方法使用 static 關鍵字來啟用對子類的表名的延後綁定引用。此方法構建一個 SQL select,然後會將域的創建延遲到 createDomain 方法。

createDomain 方法使用子類的名稱(通過 PHP V5.3 的 get_called_class 函數)來實例化適當的類。createDomain 方法之後會創建一個數組來保存數據庫列的名稱和值的一個映射。為了讓這個示例盡量簡單,ActiveRecord 並不會實際運行 SQL 代碼。同時為了讓本文的代碼能夠充分展示並測試 SQL select 的構造,ActiveRecord 具有一個在 createDomain 內設置的 $select 屬性。foreach 語句,不是設置來自 SQL 結果集的域屬性值,而是將字符串 TODO: set from sql result 填塞到字段值數組的每個元素。這個方法會返回新構建的域,而這個域又會由 findById 方法返回。代碼底部四個聲明中的第一個聲明會驗證適當的 SQL 語句是否被創建。

動態屬性和 __get

您可能會注意到的 Customer 和 Sales 域名類的一個奇怪的事情是它們並沒有任何的域屬性。您可能會期望看到如下這些行,作為 Customer 類的屬性:

$id;

$email;

$name;

這樣一來,您就可以使用以下代碼訪問這些域屬性:

$custObj->id;

$custObj->email;

$custObj->lastname;

但這些屬性是可用的;它們保存在 $fIEldvalues 數組內。並且其上的代碼也能正常工作。為了提供對域屬性的無縫訪問以便上述引用的語法能正常工作,ActiveRecord 實現了奇妙的 __get 方法。代碼底部的第二個聲明顯示了 Customer 對象是如何從 findById 方法被檢索到的,以及 email 屬性是如何被訪問的。Customer 沒有 email 屬性,但是由於定義了 __get 方法,PHP 調用了 __get 方法,並以一個參數傳遞了所請求的屬性名。之後,__get 方法只需簡單地從 $fIEldvalues 數組拉出屬性值。

注意: 生產代碼可以處理對數組內不存在的屬性的請求。

動態 finder 方法和 __callStatic

活動記錄設計模式實現內經常提供的一個很棒的特性是動態 finder 方法。沒有動態方法,域類的代碼編寫將必須考慮到類的每個數據庫查詢用戶可能會要求檢索域的一個或多個實例(並隨後編寫一個類似於 findById 的方法)。

我們來重點看看 清單 5 底部的最後一個聲明:它運行一個名為 findByLastname 的方法。但該方法不具備任何實現。此方法可能不會存在,但是由於 Customer 類的父類具有 PHP V5.3 的新 __callStatic 方法的一個實現,所以不僅不會拋出任何錯誤,而且 findByLastname 調用還會實際執行並會做一些有價值的事情。

__callStatic 方法接受兩個參數:被調用的方法名以及一個參數數組。當調用 findByLastname 時,PHP 看到此名稱不存在並會運行 ActiveRecord 的 __callStatic 方法,在第一個參數內傳遞 findByLastname,第二個參數內傳遞 Denoncourt 作為一個數組。 __callStatic 的 ActiveRecord 實現去掉 findBy 前綴後跟的字符串並將其用作 SQL where 子句內的字段名。 __callStatic 方法然後會使用 Denoncourt 實參作為對比值。有了動態調用,還可以使用如下這行代碼:

Customer::findByEmail('[email protected]');

注意您還可以增進 __callStatic 方法來支持操作符,如下所示:

Sales::findAllByQtyGreaterThan(100);

顯然,活動記錄的生產實現將遠比此更為復雜並會處理除 findBy 外的一些方法前綴。這些方法前綴可能會包含 findAllBy 來返回行的數組,使用 countBy 來返回與條件相匹配的那些行。

注意: 現在已經出現了一些利用 PHP V5.3 新特性的活動記錄框架 — Dirivante 和 PHP.activerecord —(參考資料)。

結束語

成長於 20 世紀 70 年代的我們經常會聽到一個口號“不要給我非靜態的”。但是對於 PHP V5.3,我很高興擁有很多靜態的東西 — 比如繼承樹內的靜態屬性和方法。PHP V5.3 的 LSB 功能讓您可以使用需要靜態屬性和方法的設計模式。PHP 還提供了 get_called_class 供您大量使用,因為您要實現的設計模式常常會需要衍生類的類名。有了 PHP V5.3 的奇妙 __callStatic 函數,更多的創造性在等待著您。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved