本文發表在《程序員》第7期
PHP沉思錄之二
左輕侯
PME模型
在大規模的程序設計中,組件(component)已經成爲一種很是流行的技術。常見的組件技術都基於PME模型,即屬性(Property)、方法(Method)和事件(Event)。基於PME的組件技術能夠方便地實現IoC(Inversion of Control,控制反轉),是從IDE的plugin到應用服務器的「熱發佈」等許多技術的基礎。
PHP從版本5開始,大大完善了對OO的支持,之前不能被應用的許多pattern如今均可以在PHP5中實現。所以,是否可以實現基於PHP的組件技術,也就成了一個值得討論的問題。
下面對PHP對於PME模型的支持,逐一進行討論:
屬性(Property)
PHP並不支持相似Delphi或者C#的property語法,但這並非問題。Java也不支持property語法,可是經過getXXX()和setXXX()的命名約定,一樣能夠支持屬性。PHP也能夠經過這一方式來支持屬性。可是,PHP提供了另外一種也許更好的方法,那就是__set()和__get()方法。
在PHP中,每個class都會自動繼承__set()和__get()方法。它們的定義以下:
void __set ( string name, mixed value )
mixed __get ( string name )
這兩個方法將在下列狀況下被觸發:當程序訪問一個當前類沒有顯式定義的屬性時。在這個時候,被訪問的屬性名稱做爲參數被傳入相應的方法。任何類均可以重載__set()和__get()方法,以實現本身的功能。
以下例:
class PropertyTester {
public function __get($PropName) {
echo "Getting Property $PropName\n";
}
public function __set($PropName, $Value) {
echo "Setting Property $PropName to '$Value'\n";
}
}
$Prop = new PropertyTester();
$Prop->Name;
$Prop->Name = "some string";
類PropertyTester重載了__set()和__get()方法,爲了測試,僅僅將參數打印輸出,沒有作更多的工做。測試代碼建立了PropertyTester類的實例,並試圖讀寫它並不存在的一個屬性Name。此時,__set()和__get()相繼被調用,並打印出相關參數。它的輸出結果以下:
Getting Property Name
Setting Property Name to 'some string'
基於這種機制,咱們能夠將屬性的值放在一個private的List中,在讀寫屬性時,經過重載__set()和__get()方法,讀寫List中的屬性值。
可是,__set()和__get()方法的有趣之處遠不止及。經過這兩個方法,能夠實現動態屬性,也就是不在程序中顯式定義,而是在運行時動態生成的屬性。只要想一想這種技術在OR Mapping中的做用就可以明白它的重要性了。配合__call()方法(用於實現動態方法,在下一節中詳述),它可以取代醜陋的代碼生成器(code generator)的大部分功能。
方法(Method)
PHP對方法的支持比較簡單,沒有太多能夠討論的。值得一提的是,PHP從版本5開始支持類的靜態方法(static method),這使得程序員不再用無謂地增長許多全局函數了。
事件(Event)
事件也許是PHP遇到的最複雜的問題。PHP並無在語法層面提供對事件的支持,咱們只能考慮經過別的途徑來實現。所以,咱們須要先對事件的概念和其餘語言對事件的實現方式進行討論。
事件模型能夠簡述以下:充當事件觸發者的代碼自己並不處理事件,而僅僅是在事件發生時,把程序控制權轉交給事件的處理者,在事件處理完成後,再收回控制權。事件觸發者自己並不知道事件將會被如何處理,在大多數狀況下,事件觸發者的代碼要先於事件處理者的代碼被完成。
在傳統的面向過程的語言(例如C或者PASCAL)中,事件能夠經過函數指針來實現。具體來講,事件觸發者定義一個函數指針,這個函數指針能夠在之後被指向某個處理事件的函數。在事件發生時,調用該函數指針指向的處理函數,並將事件的上下文做爲參數傳入。處理完成後,控制權再回到事件觸發者。
在面向對象的語言中,方法指針(指向某個類的方法的指針)取代了函數指針。以Delphi爲例,事件處理的例子以下:
type
TNotifyEvent = procedure(Sender: TObject) of object;
TMainForm = class(TForm)
procedure ButtonClick(Sender: TObject);
…
End;
Var
MainForm: TMainForm;
OnClick: TNotifyEvent;
…
能夠看出,TNotifyEvent被定義爲所謂的過程類型(Procedural Type),事實上就是一個方法指針。TMainForm的ButtonClick方法是一個事件處理者,符合TNotifyEvent的簽名。OnClick是一個事件觸發者。在實際使用時,經過以下代碼:
OnClick := MainForm.ButtonClick;
將MainForm.ButtonClick方法綁定到了OnClick事件。當OnClick事件觸發時,MainForm.ButtonClick方法將被調用,而且將Sender(觸發事件的組件對象)做爲參數傳入。
回到PHP,因爲PHP不支持指針,所以沒法使用函數指針這一技術。可是,PHP支持所謂的「函數變量」,能夠把函數賦予某個變量,其做用相似於函數指針。以下例:
function EventHandler($Sender) {
echo "Calling EventHandler(), arv = $Sender\n";
}
$Func = 'EventHandler';
$Func('Sender Name');
因爲PHP是一種動態語言,變量能夠爲任何類型,因此無須先定義函數指針的類型做爲事件的簽名。直接定義了一個函數EventHandler做爲事件處理者,而後將它賦予變量$Func(注意直接使用了字符串形式的函數名),最後觸發該事件,並將一個字符串「Sender Name」傳給它做爲參數。輸出的結果是:
Calling EventHandler(), arv = Sender Name
一樣地,PHP也提供了相似方法指針的機制。以下例:
Class EventHandler {
public function DoEvent($Sender) {
echo "Calling EventHandler.DoEvent(), arg = $Sender\n";
}
}
$EventHanler = new EventHandler();
$HandlerObject = $EventHanler;
$Method = 'DoEvent';
$HandlerObject->$Method('Sender Name');
因爲PHP中沒有可以直接引用對象方法的變量,所以須要使用兩個變量來間接實現:$HandlerObject指向對象,$Method指向對象方法。經過$HandlerObject->$Method方式的調用,能夠動態地指向任何對象方法。
爲了使代碼更加優雅和更適合複用,能夠定義一個專門的類NotifyEvent,並使用一段新的調用代碼:
final class NotifyEvent {
private $HandlerObject;
private $Method;
public function __construct($HandlerObject, $Method) {
$this->HandlerObject = $HandlerObject;
$this->Method = $Method;
}
public function Call($Sender) {
$Method = $this->Method;
$this->HandlerObject->$Method($Sender);
}
}
$EventHanler = new EventHandler();
$NotifyEvent = new NotifyEvent($EventHanler, 'DoEvent');
$NotifyEvent->Call('Sender Name');
NotifyEvent類定義了兩個私有變量$HandlerObject和$Method,分別指向事件處理者對象和處理方法。在構造函數中對這兩個變量賦值,再經過Call方法來調用。
熟悉C#的讀者能夠發現,NotifyEvent類與C#中的Delegate十分相似。Delegate超過NotifyEvent的地方在於支持多播(Multicast),也就是一個事件能夠綁定多個事件處理者。只要事件觸發者本身維護一個NotifyEvent對象數組,支持多播也不是一件難事。
至此,PHP對事件的支持已經獲得了比較圓滿的解決。可是,人的求知慾是無窮無盡的。還有沒有可能經過其餘的方式來實現事件呢?
除了方法指針,接口(interface)也能夠用於實現事件。在Java中,這種技術被普遍應用。其核心思想是,將事件處理者的處理函數定義抽象爲一個接口(至關於函數指針的簽名),事件觸發者針對這個接口編程,事件處理者則實現這個接口。這種方式的好處在於,不須要語言支持函數指針或方法指針,讓代碼顯得更加清晰和優雅,缺陷在於,實現同一種功能,要使用更多的代碼。以下例:
interface IEventHandler {
public function DoEvent($Sender, $Arg);
}
class EventHanlerAdapter implements IEventHandler {
public function DoEvent($Sender, $Arg) {
echo "Calling EventHanlerAdapter.DoEvent(), Sender = $Sender, arg = $Arg\n";
}
}
class EventRaiser {
private $EventHanlerVar;
public function __construct($EventHanlerAdapter) {
$this->EventHanlerVar = $EventHanlerAdapter;
}
public function RaiseEvent() {
if ($this->EventHanlerVar != null) {
$this->EventHanlerVar->DoEvent($this, 'some string');
}
}
public function __tostring() {
return 'Object EventRaier';
}
}
$EventHanlerAdapter = new EventHanlerAdapter();
$EventRaiser = new EventRaiser($EventHanlerAdapter);
$EventRaiser->RaiseEvent();
首先定義了一個接口IEventHandler,它包含了方法的簽名。EventHanlerAdapter類做爲事件處理者,實現了這個接口,並提供了相應的處理方法。EventRaiser類做爲事件觸發者,針對$EventHanlerVar變量(它應該是IEventHandler接口類型,可是在PHP中不用顯式定義)編碼。在實際應用中,將EventHanlerAdapter的實例做爲參數賦予傳給EventRaiser類的構造函數,當事件發生時,相應的處理方法將被調用。輸出結果以下:
Calling EventHanlerAdapter.DoEvent(), Sender = Object EventRaier, arg = some string
最後,讓咱們回到現實世界中來。雖然咱們用PHP完整地實現了PME模型,可是這到底有什麼用呢?畢竟,咱們不會用PHP去編寫IDE,也不會用它編寫應用服務器。回答是,基於PME模型的組件技術能夠實現更加方便和更大規模的代碼複用。在基於PHP的應用系統中,雖然插件已經被普遍使用,可是經過組件技術能夠實現功能更強大、更加規範和更容易維護的插件。此外,組件技術在實現一些大的Framework(例如,針對Web UI的Framework)時,也是不可或缺的。 程序員