對象|進階
進階PHP V5 對象研究
本文介紹了PHP V5一些更進階的面向設計的特性。其中包括各種物件類型,它們允許將系統中的組件相互分離,建立可重用、可擴充、可伸縮的代碼。
領會暗示
首先介紹一下物件類型和類型提示的優點。一個類定義一種類型。從該類執行個體化的任何對象屬於該類定義的類型。所以,使用 Car 類建立 Car 對象。如果 Car 類繼承 Vehicle 超類,則 Car 對象還將是一個 Vehicle 對象。這反映了我們在現實世界中分類事物的方法。但正如您將看到的,類型不僅僅是分類系統元素的有用方法。類型是物件導向編程的基礎,因為類型是良好一致的行為的保證。許多設計技巧來自該保證。
“開始瞭解 PHP V5 中的對象”展示對象為您保證了介面。當系統傳遞 Dictionary 對象時,您可以確定它具有 $translations 數組和 summarize() 方法。相反,關聯陣列不提供相同層級的確定性。要利用類提供的清晰介面,需要知道您的對象實際上是 Dictionary 的一個執行個體,而不是某個 imposter。可以用 instanceof 操作符來手動驗證這一點,該操作符是 PHP V5 引入的介於對象執行個體和類名之間的一個便捷工具。
instanceof Dictionary
如果給定對象是給定類的執行個體,則 instanceof 操作符解析為真。在調用方法中第一次遇到 Dictionary 對象時,可以在使用它之前檢查它的類型。
if ( $en instanceof Dictionary ) {
print $en->summarize();
}
但是,如果使用 PHP V5 的話,可以將物件類型檢查構建到類或方法聲明中。
在“開始瞭解 PHP V5 中的對象”中,重點介紹兩個類:Dictionary,它儲存術語和翻譯, DictionaryIO,它將 Dictionary 資料匯出(匯入)自(至)檔案系統。這些特性使得將 Dictionary 檔案發送到第三方翻譯器變得容易,第三方翻譯器可以使用自己的軟體來編輯資料。然後,您可以重新匯入已處理的檔案。清單 1 是 Dictionary 類的一個版本,它接受一個 DictionaryIO 對象,並將其儲存以備將來使用。
清單 1. 接受 DictionaryIO 對象的 Dictionary 類的一個版本
class Dictionary {
public $translations = array();
public $type ="En";
public $dictio;
function addDictionaryIO( $dictio ) {
$this->dictio=$dictio;
}
function export() {
if ( $this->dictio ) {
$this->dictio->export( $this );
}
}
}
class DictionaryIO {
function export( $dict ) {
print "exporting dictionary data "."($dict->type)\n";
}
}
$en = new Dictionary();
$en->addDictionaryIO( new DictionaryIO() );
$en->export();
// output:
// dumping dictionary data (En)
DictionaryIO 類具有單個方法 export(),它接受一個 Dictionary 對象,並使用它來輸出假訊息。現在,Dictionary 具有兩個新方法:addDictionaryIO(),接受並儲存 DictionaryIO 對象; export(),使用已提供的對象匯出 Dictionary 資料 —— 或者是在完全實現的版本中。
您可能會疑惑為什麼 Dictionary 對象不僅執行個體化自己的 DictionaryIO 對象,或者甚至在內部處理匯入匯出操作,而根本不求助於第二個對象。一個原因是您可能希望一個 DictionaryIO 對象使用多個 Dictionary 對象,或者希望儲存該對象的單獨引用。另一個原因是通過將 DictionaryIO 對象傳遞給 Dictionary,可以利用類切換或 多態性。換句話說,可以將 DictionaryIO 子類(比如 XmlDictionaryIO)的執行個體傳遞給 Dictionary,並更改運行時儲存和檢索資料的方法。
圖 1 顯示了 Dictionary 和 DictionaryIO 類及其使用關係。
正如所顯示的,沒有什麼阻止編碼器將完全隨機的對象傳遞給 addDictionaryIO()。只有在運行 export() 時,才會獲得一個類似的錯誤,並發現已經儲存在 $dictio 中的對象實際上並沒有 export() 方法。使用 PHP V4 時,必須測試本例中的參數類型,以絕對確保編碼器傳遞類型正確的對象。使用 PHP V5 時,可以部署參數提示來強制物件類型。只將所需的物件類型添加到方法聲明的參數變數中,如清單 2 所示:
清單 2. 將物件類型添加到方法聲明的參數變數中
function addDictionaryIO( DictionaryIO $dictio ) {
$this->dictio=$dictio;
}
function export() {
if ( $this->dictio ) {
$this->dictio->export( $this );
}
}
現在,如果客戶機編碼器試圖將類型錯誤的對象傳遞給 addDictionaryIO(),PHP 引擎將拋出一個致命錯誤。因此,類型提示使得代碼更安全。不幸的是,提示僅對對象有效,所以不能在參數列表中要求字串或整數。必須手動測試這些原類型。
即使可以保證 addDictionaryIO() 將獲得正確的物件類型,但不能保證該方法被首先調用。export() 方法測試 export() 方法中 $dictio 屬性的存在,從而避免錯誤。但您可能希望更嚴格一些,要求 DictionaryIO 對象傳遞給建構函式,從而確保 $dictio 總是被填充。
調用覆蓋方法
在清單 3 中,XmlDictionaryIO 整合 DictionaryIO。而 DictionaryIO 寫入並讀取序列化資料,XmlDictionaryIO 操作 XML,可以與第三方應用程式共用。XmlDictionaryIO 可以覆蓋其父方法(import() 和 export()),也可以選擇不提供自己的實現(path())。如果客戶機調用 XmlDictionaryIO 對象中的 path() 方法,則在 DictionaryIO 中實現的 path() 方法被調用。
事實上,可以同時使用這兩種方法。可以覆蓋方法並調用父實現。為此,使用新關鍵字 parent。用範圍解析操作符和所討論方法的名稱來使用 parent 。例如,假設需要 XmlDictionaryIO 使用當前工作目錄(如果有一個可用)中叫做 xml 的目錄;否則,它應使用由父 DictionaryIO 類產生的預設路徑,如清單 3 所示:
清單 3. XmlDictionaryIO 使用 xml 目錄或由 DictionaryIO 類產生的預設路徑
class XmlDictionaryIO extends DictionaryIO {
function path( Dictionary $dictionary, $ext ) {
$sep = DIRECTORY_SEPARATOR;
if ( is_dir( ".{$sep}xml" ) ) {
return ".{$sep}xml{$sep}{$dictionary->getType()}.$ext";
}
return parent::path( $dictionary, $ext );
}
// ...
可以看到,該方法檢查本地 xml 目錄。如果該測試失敗,則它使用 parent 關鍵字指派給父方法。
子類和建構函式方法
parent 關鍵字在建構函式方法中尤其重要。如果在子類中不定義建構函式,則 parent 建構函式代表您被顯式調用。如果在子類中不建立建構函式方法。則調用父類的建構函式並傳遞任何參數是您的責任,如清單 4 所示:
Listing 4. Invoking the parent class’s constructor
class SpecialDictionary extends Dictionary {
function __construct( $type, DictionaryIO $dictio, $additional ) {
// do something with $additional
parent::__construct( $type, $dictio );
}
}
抽象類別和方法
雖然在父類中提供預設行為是完全合法的,但這可能不是最巧妙的方法。對於啟動器,您必須依賴子類的作者來理解它們必須實現 import() 和 export(),才能在 broken 狀態建立類。而且,DictionaryIO 類實際上是兄弟,而不是父子。XmlDictionaryIO 不是 DictionaryIO 的特例;相反,它是一種備選實現。
PHP V5 允許定義部分實現的類,其主要角色是為它的子女指定核心介面。這種類必須聲明為抽象。
abstract class DictionaryIO {}
抽象類別不能執行個體化。必須建立子類(即,建立繼承它的類),並建立該子類的執行個體。可以在抽象類別中聲明標準和抽象方法,如清單 5 所示。抽象方法必須用 abstract 關鍵字限定,且必須只由一個方法簽名組成。這意味著,抽象方法應包括 abstract 關鍵字、可選的可見度修改符、function 關鍵字,以及圓括弧內可選的參數列表。它們不應有任何方法主體。
清單 5. 聲明抽象類別
abstract class DictionaryIO {
protected function path( Dictionary $dictionary,
$ext ) {
$path = Dictionary::getSaveDirectory();
$path .= DIRECTORY_SEPARATOR;
$path .= $dictionary->getType().".$ext";
return $path;
}
abstract function import( Dictionary $dictionary );
abstract function export( Dictionary $dictionary );
}
注意,path() 函數現在是受保護的。這允許來自子類的訪問,但不允許來自 DictionaryIO 類型外部的訪問。繼承 DictionaryIO 的任何類必須實現 import() 和 export() 方法,否則就可能得到致命錯誤。
聲明抽象方法的任何類本身必須是聲明為抽象的。繼承抽象類別的子類必須實現在其父類或自身中聲明為抽象的所有抽象方法。
清單 6 展示了具體的 DictionaryIO 類,為了簡潔,此處省略了實際實現。
清單 6. 具體的 DictionaryIO 類
class SerialDictionaryIO extends DictionaryIO {
function export( Dictionary $dictionary ) {
// implementation
}
function import( Dictionary $dictionary ) {
// implementation
}
}
class XmlDictionaryIO extends DictionaryIO {
protected function path( Dictionary $dictionary, $ext ) {
$path = strtolower(parent::path( $dictionary, $ext ) );
return $path;
}
function export( Dictionary $dictionary ) {
// implementation
}
function import( Dictionary $dictionary ) {
// implementation
}
}
Dictionary 類需要一個 DictionaryIO 對象傳遞到它的建構函式,但它既不知道也不關心該對象是否是 XmlDictionaryIO 或 SerialDictionaryIO 的執行個體。它惟一知道的是給定對象繼承 DictionaryIO,而且因此可以保證支援 import() 和 export() 方法。這種在運行時的類切換是物件導向編程的一個常見特性,稱為多態性。
圖 2 展示了 DictionaryIO 類。注意,抽象類別和抽象方法用斜體表示。該圖是多態性的一個好例子。它展示了 DictionaryIO 類的已定義關係是與 DictionaryIO,但 SerialDictionaryIO 或 XmlDictionaryIO 將實現該關係。
圖 2. 抽象 DictionaryIO 類及其具體子類
介面
與 Java? 程式設計語言應用程式一樣,PHP 只支援單一繼承。這意味著,類只可以繼承一個父類(雖然它可能間接地繼承許多祖先)。雖然這保證了清潔設計(clean design),但有時候您可能需要為一個類定義多個能力集。
使用對象的一個優點是類型可以為您提供功能的保證。Dictionary 對象總是具有 get() 方法,而不管它是 Dictionary 本身還是其子類的執行個體。Dictionary 的另一個特性是它對 export() 的支援。假設需要讓系統中的大量其他類同樣地可匯出。當想要將系統的狀態儲存到檔案中時,可以為這些完全不同的類提供各自的 export() 方法,然後聚集執行個體,迴圈通過所有執行個體,並為每個執行個體調用 export()。清單 7 展示了實現 export() 方法的第二個類。
清單 7. 實現 export() 方法的第二個類
class ThirdPartyNews {
// ...
}
class OurNews extends ThirdPartyNews {
// ...
function export() {
print "OurNews export\n";
}
}
注意,本例包括約束,即新類 OurNews 繼承一個叫做 ThirdPartyNews 的外部類。
清單 8 展示了聚集用 export() 方法裝備的類執行個體的類。
清單 8. 聚集用 export() 方法裝備的類執行個體的類
class Exporter {
private $exportable = array();
function add( $obj ) {
$this->exportable[] = $obj;
}
function exportAll() {
foreach ( $this->exportable as $obj ) {
$obj->export();
}
}
}
Exporter 類定義了兩個方法:add(),接受要儲存的對象,和 exportAll(),迴圈通過已儲存物件,以對每個對象調用 export()。這種設計的缺點顯而易見:add() 不檢查所提供對象的類型,所以 exportAll() 在輕快地調用 export() 時冒了致命的風險。 此處真正有用的是 add() 方法簽名中的一些類型提示。Dictionary 和 OurNews 專用於不同的根。您可以依賴 add() 方法內部的類型檢查,但這並不優雅而且不固定。每次建立支援 export() 的新類型時,就需要建立一個新類型檢查。
介面免去了這種麻煩。正如名稱所表明的,介面定義功能而非實現。用 interface 關鍵字聲明介面。
interface Exportable {
public function export();
}
對於抽象類別,可以定義任意數目的方法簽名。子類必須提供每個方法的實現。但是,與抽象類別不同,介面完全不能包含任何具體方法(也就是說,任何方法的任何特性都不能與其簽名分離)。 類用 implements 關鍵字實現介面,如清單 9 所示。
清單 9. 用 implements 關鍵字實現介面的類
class OurNews extends ThirdPartyNews
implements Exportable {
// ...
function export() {
print "OurNews export\n";
}
}
class Dictionary implements Exportable, Iterator {
function export() {
//...
}
}
通過在 implements 後使用逗號分隔的列表,可以實現任意多的介面。必須實現每個介面中聲明的所有方法,或者聲明您的實作類別抽象。 這樣做可以得到什麼呢?現在,Dictionary 和 OurNews 對象共用類型。所有此類對象還是 Exportable。可以用類型提示和 instanceof 測試來檢查它們。清單 10 展示了修改後的 Exporter::add() 方法。
清單 10. 修改後的 Exporter::add() 方法
class Exporter {
private $exportable = array();
function add( Exportable $obj ) {
$this->exportable[] = $obj;
}
//...
介面是一個難以掌握的概念。畢竟,它們實際上並不提供任何有用代碼。竅門是記住物件導向編程中類型的重要性。介面與合約類似。它借給類一個將類放置到位的名稱,反過來,該類保證特定方法將可用。此外,使用 Exportable 對象的類既不知道也不關心調用 export() 時發生的行為。它只知道它可以安全地調用該方法。
圖 3 顯示了 Exportable 介面與其實作類別之間的關係。注意到 Exporter 與 Exportable 介面而非具體實現有使用關係。介面關係用虛線和開箭頭表示。
結束語
本文支援使用 PHP V5 中類型的價值。物件類型允許將系統中的組件相互分離,從而得到可重用、可擴充和可伸縮的代碼。抽象類別和介面協助您基於類類型設計系統。 客戶機類可被編碼為只需要抽象類別型,而把實現策略和結果留給在運行時傳遞給它們的具體類執行個體。也就是說,Dictionary 既不局限於序列化資料,也不局限於 XML。如果必須支援一種新格式,Dictionary 將不需要任何進一步的開發。它與儲存資料以及從檔案系統載入資料和將資料載入到檔案系統的機制完全無關。Dictionary 只知道它必須具有一個 DictionaryIO 對象,從而保證 export() 和 import() 的功能。
如果類保證了介面,您必須能夠保證類。雖然 instanceof 功能提供了一種檢查類型的好方法,但您還可以通過在參數列表中使用類型提示,來將物件類型檢查滾動到方法簽名自身中