《PHP設計模式介紹》第九章 觀測模式_PHP教程
推薦:《PHP設計模式介紹》第八章 迭代器模式類中的面向?qū)ο缶幊谭庋b應用邏輯。類,就是實例化的對象,每個單獨的對象都有一個特定的身份和狀態(tài)。單獨的對象是一種組織代碼的有用方法,但通常你會處理一組對象或者集合。 屬性來自 SQL 查
一些面向?qū)ο蟮木幊谭绞�,提供了一種構建對象間復雜網(wǎng)絡互連的能力。當對象們連接在一起時,它們就可以相互提供服務和信息。
通常來說,當某個對象的狀態(tài)發(fā)生改變時,你仍然需要對象之間能互相通信。但是出于各種原因,你也許并不愿意因為代碼環(huán)境的改變而對代碼做大的修改。也許,你只想根據(jù)你的具體應用環(huán)境而改進通信代碼�;蛘�,你只想簡單的重新構造通信代碼來避免類和類之間的相互依賴與相互從屬。
問題
當一個對象的狀態(tài)發(fā)生改變時,你如何通知其他對象?是否需要一個動態(tài)方案――一個就像允許腳本的執(zhí)行一樣,允許自由連接的方案?
解決方案
觀測模式允許一個對象關注其他對象的狀態(tài),并且,觀測模式還為被觀測者提供了一種觀測結構,或者說是一個主體和一個客體。主體,也就是被觀測者,可以用來聯(lián)系所有的觀測它的觀測者。客體,也就是觀測者,用來接受主體狀態(tài)的改變
觀測就是一個可被觀測的類(也就是主題)與一個或多個觀測它的類(也就是客體)的協(xié)作。不論什么時候,當被觀測對象的狀態(tài)變化時,所有注冊過的觀測者都會得到通知。
觀測模式將被觀測者(主體)從觀測者(客體)種分離出來。這樣,每個觀測者都可以根據(jù)主體的變化分別采取各自的操作。(觀測模式和Publish/Subscribe模式一樣,也是一種有效描述對象間相互作用的模式。)
觀測模式靈活而且功能強大。對于被觀測者來說,那些查詢哪些類需要自己的狀態(tài)信息和每次使用那些狀態(tài)信息的額外資源開銷已經(jīng)不存在了。另外,一個觀測者可以在任何合適的時候進行注冊和取消注冊。你也可以定義多個具體的觀測類,以便在實際應用中執(zhí)行不同的操作。
實例代碼
舉例來說,你可以使用觀測模式為你的PHP腳本來創(chuàng)建一個更靈活的記錄錯誤的句柄。因為,默認的錯誤記錄句柄也許只會在屏幕上顯示一些出錯信息,但是增強后的句柄還可以將出錯信息寫進一個日志文件中,或?qū)⒊鲥e信息寫進系統(tǒng)日志之中,或?qū)⒊鲥e信息通過電子郵件發(fā)送出去,或利用聲音報告出錯信息。你甚至還可以構造一種有級別的報錯方案,只允許向那些已經(jīng)為具體的出錯信息注冊過的觀測者報告。從一般的警告信息到像數(shù)據(jù)庫失靈之類的嚴重出錯信息都可以報告。
下面,我們用觀測模式來為PHP創(chuàng)建一系列的類來實現(xiàn)剛才所說的那些功能。新建一個名為ErrorHandler的類,它就是觀測模式的主體,也就是被觀測者。再建另外兩個名為FileErrorLogger和EmailErrorLogger的類,它們是觀測客體(即觀測者)。FileErrorLogger類將出錯信息寫入日志文件,EmailErrorLogger類利用電子郵件發(fā)送出錯信息。在UML中,可以表示如下:
為了實現(xiàn)以觀測模式為基礎的錯誤記錄句柄,首先我們注意到作為觀測者的FileErrorLogger類和EmailErrorLogger類什么也不能做。那么,F(xiàn)ileErrorLogger類是如何向一個文件寫出錯信息,EmailErrorLogger類又如何發(fā)送電子郵件的?接下來,讓我來看看用來實現(xiàn)觀測模式的技術細節(jié),然后,再集中精力來看看該模式的主體――ErrorHandler的細節(jié)。最后,再寫一些錯誤處理函數(shù)來調(diào)用這個ErrorHandler類。
最后用下面的這一段代碼來表示:
//PHP4
$eh=&getErrorHandlerInstance();
$eh->attach(newEmailErrorLogger(‘[email protected]’));
$eh->attach(newFileErrorLogger(fopen(‘error.log’,’w’)));
set_error_handler(‘observer_error_handler’);
//...later
trigger_error(‘thisisanerror’);
ErrorHandler類是一種單件模式(參考第4章:TheSingletonPattern)。它可以通過函數(shù)Attach()來注冊各種錯誤信息觀測者,而set_error_handler()函數(shù)就是一個指向ErrorHandler類的函數(shù)。最后,當一個錯誤信息被觸發(fā)后,所有的觀測者都會得到通知。
為了使這次觀測的操作生效,你的測試必須能證明所有的這些操作(將錯誤信息寫入日志,利用電子郵件發(fā)送錯誤信息)都能得到執(zhí)行,并且能正常工作。簡而言之,讓我們來看看一系列簡單的測試。(和這個實例有關的其他更多實例,可以在本書附帶的源代碼中找到)
這里有FileErrorLogger類聯(lián)合測試的一部分代碼:它用來測試當FileErrorlogger類被某個對象實例化時,是否具有向一個文件寫日志的能力。
classFileErrorLoggerTestCaseextendsUnitTestCase{
var$_fh;
var$_test_file=‘test.log’;
functionsetup(){
@unlink($this->_test_file);
$this->_fh=fopen($this->_test_file,‘w’);
}
functionTestRequiresFileHandleToInstantiate(){/*...*/}
functionTestWrite(){
$content=‘test’.rand(10,100);
$log=&newFileErrorLogger($this->_fh);
$log->write($content);
$file_contents=file_get_contents($this->_test_file);
$this->assertWantedPattern(‘/’.$content.’$/’,$file_contents);
}
functionTestWriteIsTimeStamped(){/*...*/}
}
在這個測試中,setup()函數(shù)創(chuàng)建了一個文件指針,指向一個名為“test.log”的新文件。并且,將該指針保存在變量$_fh中,這個可寫的文件指針將作為一個變量傳遞給FileErrorlogger對象的實例,進行測試。變量$content的值將傳遞給函數(shù)write(),并且,在存儲結束后,還將用來被檢查$content的值是否確實被正確寫入test.log文件中。
(這個測試要求PHP必須具有向那個新建的test.log中寫數(shù)據(jù)的權限。)
下面的一些代碼也許可以幫助FileErrorLogger類通過測試。
classFileErrorLogger{
var$_fh;
functionFileErrorLogger($file_handle){
$this->_fh=$file_handle;
}
functionwrite($msg){
fwrite($this->_fh,date(‘Y-m-dH:i:s:‘).$msg);
}
}
一個類似的測試代碼可以使EmailErrorLogger類生效。
classEmailErrorLoggerTestCaseextendsUnitTestCase{
functionTestEmailAddressFirstConstructorParameter(){
$log=&newEmailErrorLogger;
$this->assertErrorPattern(‘/missing.*1/i’);
}
functionTestMail(){
$log=&newEmailErrorLogger(‘[email protected]’);
$log->mail(‘testmessage’);
}
}
classEmailErrorLogger{
var$_addr;
var$_subject;
functionEmailErrorLogger($addr,
$subject=’ApplicationErrorMessage’){
$this->_addr=$addr;
$this->_subject=$subject;
}
functionmail($msg){
mail($this->_addr
,$this->_subject
,date(‘Y-m-dH:i:s:‘).$msg);
}
}
你是怎樣確定EmailErrorLogger類能真正發(fā)送電子郵件的呢?是的,你可以打開你的收件箱,看看其中是否有新郵件,就知道了。但是,那就不是一個全自動的測試了�;蛘哒f,這個測試就只是偽模式的一個不錯的替代方案。(至于如何創(chuàng)建一個控制郵件的類,將作為一個練習留給讀者的。詳細信息,請參考第6章TheMockObjectPattern或參考FakeMail項目http://sf.net/projects/fakemail/.)
有了合適而正確的觀測者,我們就可以在觀測模式下,從函數(shù)attach()開始繼續(xù)測試ErrorHandler類。
classObserver{
functionupdate(){
die(‘abstractmethod’);
}
}
Mock::Generate(‘Observer’);
classErrorHandlerTestCaseextendsUnitTestCase{
functionTestAttach(){
$eh=&newErrorHandler;
$observer=&newMockObserver($this);
$observer->expectOnce(
‘update’
,array(‘*’));//array(&$eh)
$eh->attach($observer);
$eh->notify();
$observer->tally();
}
functionTestDetach(){/*...*/}
}
在這次測試中,一個簡單的觀測類被創(chuàng)建出來,作為所有觀測者的接口。為了測試函數(shù)attach(),一個基于這個觀測類的偽模式被創(chuàng)建出來,并且和ErrorHandler測試實例關聯(lián)在一起。然后,當公共函數(shù)notify()被調(diào)用時,偽模式將證實update()函數(shù)曾經(jīng)被調(diào)用過。
請注意剛才提及的的在模擬觀測中所創(chuàng)建的函數(shù)array(&$eh)中的參數(shù)。在理想狀態(tài)中,那個測試應該可以通過的。然而,由于PHP語言的限制,這將產(chǎn)生一個致命錯誤:“NestingLevelTooDeep――循環(huán)依賴?”。為了避免出現(xiàn)那樣的問題,代碼中必須使用簡單測試下“WildCard”功能,以便允許所有參數(shù)都能像預期的那樣傳遞。
NestingLevelTooDeep
因為ErrorHandler在數(shù)組$_observer中包含涉及到模擬觀測的參數(shù),本來預期是要將它傳遞給模擬觀測的。所以,PHP產(chǎn)生一個“NestingLevelTooDeep”錯誤。而循環(huán)依賴就像一個初級的PHP問題,甚至可以在一個簡單的PHP環(huán)境中發(fā)現(xiàn)它。(請參考http://bugs.php.net/bug.php?id=31449.)
分享:《PHP設計模式介紹》第七章 策略模式在編寫面向?qū)ο蟮拇a的時,有些時候你需要一個能夠自己根據(jù)不同的條件來引入不同的操作對象實例。例如,一個菜單功能能夠根據(jù)用戶的“皮膚”首選項來決定是否采用水平的還是垂直的排
- 相關鏈接:
- 教程說明:
PHP教程-《PHP設計模式介紹》第九章 觀測模式
。