Observer模式深度探索
2024-07-21 02:16:27
供稿:網友
 
,歡迎訪問網頁設計愛好者web開發。[簡介]
微軟頂級技術大師jeffrey richter的作品,一向是不容錯過的。為了幫助開發者這篇專論observer模式的文章也不例外。observer模式是經典設計模式中應用最為廣泛也最為靈活多變的模式之一。本文在.net技術框架下深入發掘了observer模式的內涵,值得細細品味。
雖然設計模式并不是萬能丹,但確實是一個非常強大的工具,開發人員或架構師可使用它積極地參與任何項目。設計模式可確保通過熟知和公認的解決方案解決常見問題。模式存在的事實基礎在于:大多數問題,可能已經有其他個人或開發小組解決過了。因此,模式提供了一種在開發人員和組織之間共享可使用解決方案的形式。無論這些模式的出處是什么,這些模式都利用了大家所積累的知識和經驗。這可確保更快地開發正確的代碼,并降低在設計或實現中出現錯誤的可能性。此外,設計模式在工程小組成員之間提供了通用的術語。參加過大型開發項目的人員都知道,使用一組共同的設計術語和準則對成功完成項目來說是至關重要的。最重要的是,如果能正確地使用,設計模式可以節省您大量的時間。
.net框架模式
雖然gof的示例僅限于c++和smalltalk,但設計模式并不與特定語言或開發平臺捆綁在一起;microsoft .net框架的出現為分析設計模式提供了新的機會和環境。在框架類庫(fcl)的開發過程中,microsoft應用了很多gof模式。由于.net框架中提供的功能范圍非常廣泛,因此,還開發和提出了一些全新的模式。
我們對設計模式的研究從observer模式入手。
observer模式
面向對象的開發的一個主導原則是,在給定的應用程序中正確地劃分任務。系統中的每個對象應該將重點放在問題域中的離散抽象上。簡而言之,一個對象只應做一件事,而且要將它做好。這種方法可確保在對象之間劃定清晰的界限,因而可提供更高的重用性和系統可維護性。
一個特別重要的領域是用戶界面和基礎業務邏輯之間的交互。在應用程序的開發過程中,需要快速更改用戶界面,并且不能對應用程序的其他部分產生連帶影響,這是司空見慣的事。此外,業務要求也可能會發生變化,而這一切與用戶界面無關。具有豐富開發經驗的人都知道,在很多情況下,這兩組要求都會發生變化。如果沒有劃分ui和應用程序其他部分,修改任一部分都會對整體造成不利的影響。
很多應用程序都需要在用戶界面和業務邏輯之間劃分清晰的界限。因此,自gui出現以后,很多面向對象的框架均支持將用戶界面從應用程序的其他部分中劃分出來。其中的大部分應用程序采用的設計模式幾乎相同。這種模式通常稱為觀察者,它非常有助于在系統中各種對象之間劃分清晰的界限。此外,還會經常看到在框架或應用程序中與ui無關的部分中使用這種解決方案。observer模式的作用遠遠超過了其最初的想法。
邏輯模型
雖然observer模式有很多變體,但該模式的基本前提包含兩個角色:觀察者(observer)和主體(subject)(熟悉smalltalk mvc的人將這些術語分別稱為view和model)。在用戶界面的環境中,觀察者是負責向用戶顯示數據的對象。另一方面,主體表示從問題域中模擬的業務抽象。正如圖1中所描述的一樣,在觀察者和主體之間存在邏輯關聯。當主體對象中發生更改時,(例如,修改實例變量),觀察者就會觀察這種更改,并相應地更新其顯示。
例如,假定我們要開發一種簡單的應用程序,來跟蹤全天的股票價格。在此應用程序中,我們指定一個stock類來模擬在nasdaq交易的各種股票。該類包含一個實例變量,它表示在全天不同時段經常波動的股價。為了向用戶顯示此信息,應用程序使用一個stockdisplay類向stdout(標準輸出)寫信息。在此應用程序中,一個stock類實例作為主體,一個stockdisplay類實例作為觀察者。隨著股價在交易日中隨時間發生變化,stock實例的當前股價也會發生變化(它怎樣變化并不重要)。因為stockdisplay實例正在觀察stock實例,所以在這些狀態發生變化(修改股價)時,就會向用戶顯示這些變化。
通過使用這種觀察過程,可以在stock和stockdisplay類之間劃分界限。假定應用程序的要求第二天發生變化,要使用基于窗體的用戶界面。要啟用此新功能,只需要構造一個新類stockform作為觀察者。無論發生什么情況,stock類都不需要進行任何修改。事實上,它甚至不知道發生此類更改。類似地,如果需求變化要求stock類從另一個來源檢索股價信息(可能是從web服務,而不是從數據庫中檢索),則stockdisplay類不需要進行修改。它只是繼續觀察stock就夠了。
物理模型
正如大多數解決方案一樣,問題在于細節。observer模式也不例外。雖然邏輯模型規定觀察者觀察主體;但在實現這種模式時,這實際上是一個名稱誤用。更準確地說,觀察者向主體注冊,表明它觀察主體的意愿。在某種狀態發生變化時,主體向觀察者通知這種變化情況。當觀察者不再希望觀察主體時,觀察者向主體撤消注冊。這些步驟分別稱為觀察者注冊、通知和撤消注冊。
大多數框架通過回調來實現注冊和通知。圖2、3和4中所示的uml序列圖模擬這種方法通常使用的對象和方法調用。對于不熟悉序列圖的人來說,最上面的矩形框表示對象,而箭頭表示方法調用。
圖2描述了注冊序列。觀察者對主體調用register方法,以將其自身作為參數傳遞。在主體收到此引用后,它必須將其存儲起來,以便在將來某個時間狀態發生變化時通知觀察者。大多數觀察者實現并非將觀察者引用直接存儲在實例變量中,而是將此任務委托給一個單獨的對象(通常為一個容器)。使用容器來存儲觀察者實例可提供非常大的好處,我們將對它進行簡要介紹。
圖3突出顯示了通知序列。當狀態發生變化時(askprice
changed),主體通過調用get觀察者s方法來檢索容器中的所有觀察者。主體然后枚舉檢索的觀察者,并調用notify方法以通知觀察者所發生的狀態變化。
圖4顯示撤消注冊序列。此序列是在觀察者不再需要觀察主體時執行的。觀察者調用unregister方法,并將其自身作為參數進行傳遞。然后,主體對容器調用remove方法以結束觀察過程。
回到我們的股票應用程序,讓我們分析一下注冊和通知過程所產生的影響。在應用程序啟動過程中,一個stockdisplay類實例注冊到stock實例中,并將其自身作為參數傳遞到register方法。stock實例(在容器中)保存對stockdisplay實例的引用。當股價屬性發生變化時,stock實例通過調用notify方法向stockdisplay通知所發生的變化。在應用程序關閉時,stockdisplay實例使用以下方法撤消注冊stock實例:調用unregister方法,終止兩個實例之間的關系。
請注意利用容器(而不是使用實例變量)來存儲觀察者引用有什么優點。假定除當前用戶接口stockdisplay外,我們還需要繪制股價在交易日內變化的實時圖形。為此,我們創建了一個名為stockgraph的新類,它繪制股價(y軸)和當天時間(x軸)的圖形。在應用程序啟動時,它同時在stock實例中注冊stockdisplay和stockgraph類的實例。因為主體在容器(與實例變量相對)中存儲觀察者,所以這不會出現問題。當股價發生變化時,stock實例向其容器中的兩個觀察者實例通知所發生的狀態變化。正如我們所看到的一樣,使用容器可提供更大的靈活性,即每個主體可支持多個觀察者。這使主體有可能向無數多個觀察者通知所發生的狀態變化,而不是只通知一個觀察者。
雖然不是強制要求,但很多框架為觀察者和主體提供了一組要實現的接口。正如下面的c#代碼示例所示,iobserver接口公開一種公共方法notify。此接口是由所有要用作觀察者的類實現的。iobservable接口(是由所有要用作主體的類實現的)公開兩種方法register和unregister。這些接口通常采用抽象虛擬類或真實接口的形式(如果實現語言支持此類構造的話)。利用這些接口有助于減少觀察者和主體之間的耦合關系。與觀察者和主體類之間的緊密耦合關系不同,iobserver和iobservable接口允許執行獨立于實現的操作。通過對接口的分析,您將注意到鍵入的所有方法針對的是接口類型(與具體類相對)。這種方法將接口編程模型的優點擴展到observer模式。
iobserver和iobservable接口(c#)
//interface the all observer classes should implement
public interface iobserver {
 
 void notify(object anobject);
 
}//iobserver
//interface that all observable classes should implement
public interface iobservable {
 void register(iobserver anobserver);
 void unregister(iobserver anobserver);
}//iobservable
再回到我們的示例應用程序,我們知道stock類用作主體。因此,它將實現iobservable接口。類似地,stockdisplay類實現iobserver接口。因為所有操作都是由該接口定義的(而不是由具體類定義的),所以stock類并未與stockdisplay類綁定在一起,反之亦然。這使我們能夠快速地更改特定的觀察者或主體實現,而不會影響應用程序的其他部分(使用不同的觀察者替換stockdisplay或添加額外的觀察者實例)。
除了這些接口外,框架還經常為主體提供一個通用基類,減少了支持observer模式所需的工作。基類實現iobservable接口,以提供支持觀察者實例存儲和通知所需的基礎結構。下面的c#代碼示例簡要介紹一個名為observableimpl的基類。盡管可能任何容器都可以完成這一任務,但該類在register和unregister方法中將觀察者存儲委托給哈希表實例(為了方便起見,我們在示例中使用哈希表作為容器,它只使用一個方法調用來撤消注冊特定的觀察者實例)。還要注意添加了notifyobservers方法。此方法用于通知哈希表中存儲的觀察者。在調用此方法時,將枚舉該容器,并對觀察者實例調用notify方法。
observableimpl類(c#)
//helper class that implements observable interface
public class observableimpl:iobservable {
 
 //container to store the observer instance (is not synchronized for 
 this example)
 protected hashtable _observercontainer=new hashtable();
 
 //add the observer
 public void register(iobserver anobserver){
 _observercontainer.add(anobserver,anobserver); 
 }//register
 
 //remove the observer
 public void unregister(iobserver anobserver){
 _observercontainer.remove(anobserver); 
 }//unregister
 //common method to notify all the observers
 public void notifyobservers(object anobject) { 
 
 //enumeration the observers and invoke their notify method
 foreach(iobserver anobserver in _observercontainer.keys) { 
 anobserver.notify(anobject); 
 }//foreach
 
 }//notifyobservers
}//observableimpl
我們的示例應用程序使用以下方法來利用此基類基礎結構:修改stock類以擴展observableimpl類,而不是提供其自己的特定iobservable接口實現。因為observableimpl類實現了iobservable接口,所以不需要對stockdisplay類進行任何更改。實際上,這種方法簡化了observer模式的實現,在保持類之間松散耦合關系的同時,使多個主體重復使用相同的功能。
下面的.net觀察者示例重點說明了iobservable和iobserver接口以及observablebase類在我們的股票應用程序中的使用情況。除了stock和stockdisplay類外,此示例使用mainclass將觀察者和主體實例關聯起來,并修改stock實例的askprice屬性。此屬性負責調用基類的notifyobservers方法,而該方法又向該實例通知相關的狀態變化。
觀察者示例(c#)
//represents a stock in an application
public class stock:observableimpl {
 
 //instance variable for ask price
 object _askprice;
 //property for ask price
 public object askprice {
 
 set { _askprice=value;
 base.notifyobservers(_askprice);
 }//set
 
 }//askprice property
 
}//stock
//represents the user interface in the application
public class stockdisplay:iobserver {
 public void notify(object anobject){ 
 console.writeline("the new ask price is:" + anobject); 
 }//notify
}//stockdisplay
public class mainclass{
 public static void main() {
 //create new display and stock instances
 stockdisplay stockdisplay=new stockdisplay();
 stock stock=new stock();
 //register the grid
 stock.register(stockdisplay);
 //loop 100 times and modify the ask price
 for(int looper=0;looper < 100;looper++) {
 stock.askprice=looper;
 }
 //unregister the display
 stock.unregister(stockdisplay);
 
 }//main
 
}//mainclass
.net框架中的observer模式
基于我們對observer模式的了解,讓我們將注意力轉向此模式在.net框架中的使用情況。您們當中非常熟悉fcl中所公開類型的人將會注意到,框架中沒有iobserver、iobservable或observableimpl類型。雖然您的確可以在.net應用程序中使用這些構造,但引入委托和事件可提供新的、功能強大的方法來實現observer模式,而不必開發專用于支持該模式的特定類型。事實上,因為委托和事件是clr的一級成員,所以將此模式的基本構造添加到.net框架的核心中。因此,fcl在其結構中廣泛使用observer模式。
介紹委托和事件內部工作方式的文章非常多,我們在此不再贅述。我們只需說明委托是面向對象(和類型安全)版的函數指針就夠了。委托實例保存對實例或類方法的引用,允許匿名調用綁定方法。事件是在類上聲明的特殊構造,可在運行時發布被關注對象的狀態變化。事件表示我們前面用于實現observer模式的注冊、撤消注冊和通知方法的形式抽象(clr和多種不同的編譯器對它提供支持)。委托是在運行時注冊到特定事件中的。在引發事件時,將調用所有注冊的委托,以使它們能夠收到事件的通知。
按照observer模式定義的術語,聲明事件的類就是主體。與我們以前使用的iobservable接口和observableimpl類不同,主體類不需要實現給定接口或擴展基類。主體只需要公開一個事件,而不需要執行任何其他操作。觀察者創建的工作略多一些,但靈活性卻提高得非常多(我們將在后面討論)。觀察者并不實現iobserver接口和將其自身注冊到主體中,而是必須創建特定的委托實例,并將此委托注冊到主體事件中。觀察者必須使用具有事件聲明所指定類型的委托實例,否則,注冊就會失敗。在創建此委托實例的過程中,觀察者將傳遞該主體向委托通知的方法(實例或靜態)名稱。在將委托綁定到方法后,可以將其注冊到主體的事件中。類似地,也可以從事件中撤消注冊此委托。主體通過調用事件向觀察者提供通知。
如果您不熟悉委托和事件,則實現observer模式似乎需要做很多工作,尤其是與我們以前使用的iobserver和iobservable接口相比。但是,它比聽起來要簡單一些,并且實現起來要容易得多。下面的c#和visual basic .net代碼示例重點說明了在我們的示例應用程序中支持委托和事件所需的類修改。注意,沒有stock或stockdisplay類用于支持該模式的任何基類或接口。
使用委托和事件的觀察者(c#)
public class stock {
 //declare a delegate for the event
 public delegate void askpricedelegate(object aprice);
 //declare the event using the delegate
 public event askpricedelegate askpricechanged;
 //instance variable for ask price
 object _askprice;
 //property for ask price
 public object askprice {
 
 set { 
 //set the instance variable
 _askprice=value; 
 //fire the event
 askpricechanged(_askprice); 
 }
 
 }//askprice property
 
}//stock class
//represents the user interface in the application
public class stockdisplay {
 public void askpricechanged(object aprice) {
 console.write("the new ask price is:" + aprice + "/r/n"); }
}//stockdispslay class
public class mainclass {
 public static void main(){
 //create new display and stock instances
 stockdisplay stockdisplay=new stockdisplay();
 stock stock=new stock();
 
 //create a new delegate instance and bind it
 //to the observer's askpricechanged method
 stock.askpricedelegate adelegate=new
 stock.askpricedelegate(stockdisplay.askpricechanged);
 
 //add the delegate to the event
 stock.askpricechanged+=adelegate;
 //loop 100 times and modify the ask price
 for(int looper=0;looper < 100;looper++) {
 stock.askprice=looper;
 }
 //remove the delegate from the event
 stock.askpricechanged-=adelegate;
 }//main
}//mainclass
在熟悉了委托和事件后,您就會清楚地看到它們的巨大潛力。與iobserver和iobservable接口以及observableimpl類不同,使用委托和事件可大大減少實現此模式所需的工作量。clr和編譯器為觀察者容器管理提供了基礎,并且為注冊、撤消注冊和通知觀察者提供了一個通用調用約定。也許,委托的最大優點是其能夠引用任何方法的固有特性(條件是它符合相同的簽名)。這允許任何類用作觀察者,而與它所實現的接口或它專用的類無關。雖然使用iobserver和iobservable接口可減少觀察者和主體類之間的耦合關系,但使用委托可完全消除這些耦合關系。
事件模式
基于事件和委托,fcl可以非常廣泛地使用observer模式。fcl的設計者充分認識到此模式的巨大潛力,并在整個框架中將其應用于用戶界面和非ui特定的功能。但是,用法與基本observer模式稍有不同,框架小組將其稱為事件模式。
通常,將此模式表示為事件通知進程中所涉及的委托、事件和相關方法的正式命名約定。雖然clr或標準編譯器并沒有強制要求利用事件和委托的所有應用程序和框架都采用這種模式,但microsoft建議這樣做。
其中的第一條約定也可能是最重要的約定是主體公開的事件的名稱。對于它所表示的狀態變化而言,此名稱應該是不言自明的。切記,此約定以及所有其他此類約定本身就是主觀性的。目的是為那些利用您的事件的人員提供清晰的說明。事件模式的其他部分利用正確的事件命名,因而此步驟對模式來說至關重要。
回到我們的示例,讓我們分析一下這種約定對stock類產生的影響。派生事件名稱的適當方法是,利用在主體類中修改的字段的名稱作為根。因為在stock類中修改的字段名稱是_askprice,所以合理的事件名稱應該是askpricechanged。很明顯,此事件的名稱比statechangedinstockclass等具有更強的描述性。因此,askpricechanged事件名稱符合第一條約定。
事件模式中的第二條約定是正確命名委托及其簽名。委托名稱應該包含事件名稱(通過第一個約定選擇的)及附加詞handler。此模式要求委托指定兩個參數,第一個參數提供對事件發送方的引用,第二個參數向觀察者提供環境信息。第一個參數的名稱就是sender。必須將此參數鍵入為system.object。這是由于以下事實:可能將委托綁定到系統中任何類上的任何潛在方法。第二個參數的名稱(甚至比第一個參數更簡單)為e。必須將此參數鍵入為system.eventargs或某種派生類(有時比此內容還多)。雖然委托的返回類型取決于您的實現需要,但大多數實現此模式的委托根本不返回任何值。
需要稍加注意委托的第二個參數e。此參數允許主體對象將任意環境信息傳遞給觀察者。如果不需要此類信息,則使用system.eventargs實例就足夠了,因為此類的實例表示沒有環境數據。否則,應該使用相應的實現構造從system.eventargs派生的類以提供此數據。必須按照具有附加詞eventargs的事件名稱來命名該類。
請參考我們的stock類,此約定要求將處理askpricechanged事件的委托命名為askpricechangedhandler。此外,應該將此委托的第二個參數命名為askpricechangedeventargs。因為我們需要將新的股價傳遞給觀察者,所以我們需要擴展system.eventargs類,以將該類命名為askpricechangedeventargs并提供實現來支持傳遞此數據。
事件模式中的最后一個約定是負責引發事件的主體類上方法的名稱和可訪問性。此方法的名稱應該包含事件名稱以及添加的on前綴。應該將此方法的可訪問性設置為保護。此約定僅適用于非密封(在vb中不可繼承)類,因為它作為派生類調用在基類中注冊的觀察者的已知的調用點。
將此最后一條約定應用于stock類,即可完成事件模式。因為stock類不是密封的,所以我們必須添加一種方法來引發事件。按照該模式,此方法的名稱為onaskpricechanged。下面的c#代碼示例顯示應用于stock類的事件模式的完整視圖。請注意我們的system.eventargs類的專門用法。
事件模式示例(c#)
public class stock {
 //declare a delegate for the event
 public delegate void askpricechangedhandler(object sender, 
 askpricechangedeventargs e);
 //declare the event using the delegate
public event askpricechangedhandler askpricechanged;
 //instance variable for ask price
 object _askprice;
 //property for ask price
 public object askprice {
 set { 
 //set the instance variable
_askprice=value; 
//fire the event
onaskpricechanged(); 
 }
 
 }//askprice property
 
 //method to fire event delegate with proper name
 protected void onaskpricechanged() {
 askpricechanged(this,new askpricechangedeventargs(_askprice));
 }//askpricechanged
 }//stock class
 //specialized event class for the askpricechanged event
 public class askpricechangedeventargs:eventargs {
 //instance variable to store the ask price
 private object _askprice;
 //constructor that sets askprice
 public askpricechangedeventargs(object askprice) { _askprice=askprice; }
 //public property for the ask price
 public object askprice { get { return _askprice; } }
 }//askpricechangedeventargs
結論
基于這里對observer模式的分析,我們可以清楚地看到此模式提供了一個完美的機制,能夠在應用程序中的對象之間劃定清晰的界限。雖然通過回調進行實現(使用iobserver和iobservable接口)相當簡單,但clr的委托和事件概念可處理大多數“繁重的工作”,并降低主體和觀察者之間的耦合級別。實際上,通過正確地使用此模式,在確保應用程序可演變性方面就會向前邁出一大步。當您的ui和業務要求隨時間發生變化時,observer模式可確保能夠簡化您的工作。
在開發靈活的應用程序方面,設計模式是一個非常強大的工具(如果有效地加以運用)。撰寫本文是為了說明模式方法的有效性,并重點說明.net框架中使用的一種模式。將來的文章將繼續探究fcl中的模式,并簡要介紹一些用于生成有效web服務的模式。