代碼執行結果如下圖:
代碼說明
l 在這裡,我們使用C#語言事件機制來實現觀察者模式,雖然和GOF的“標准”模式不同,但是還是可以看出觀察者模式最基本的幾個角色。要知道,GOF設計模式雖然是經典,但是畢竟是很久以前提出的,可以考慮使用C#的一些特性來改進。
l Pager類型是抽象主體角色(或者叫作被觀察者、發布方、主動方、目標、主題),傳統的抽象主體用於保存觀察者。在這裡的ChangePage方法用於在有變化後觸發事件。另外,從ChangePageHandler代理中看到,我們把抽象主體作為了參數,這樣,觀察者就能根據主體的狀態作一些調整。
l ButtonPage是一個具體主體角色。NextPage()方法中首先判斷請求的頁面是否超過了頁面索引,如果沒有超過的話,則更新頁面索引並且調用了基類的ChangePage()方法來通知所有的觀察者。PreviousPage()方法也是一樣的道理。
l Control接口是一個抽象觀察者角色(或者說觀察者、訂閱方、被動方),它定義了一個統一的接口,如果接受到了事件通知,則調用這個方法進行處理。
l GridView和Label則是具體觀察者,可以看到它們不用考慮怎麼被通知的事情,只需要考慮被通知後做什麼。在這裡,GridVIEw重新綁定了數據,Label顯示了頁數信息。
l 這樣其實已經組成了一個最基本的觀察者模式的結構。獲取你也注意到了,ButtonPager還實現了Control接口,說明它還是一個具體的觀察者。這並沒有什麼不可以,它一方面可以在翻頁後通知GridVIEw、Label等對象,一方面又可以被別人通知。還記得客戶需要實現一個ListPager的需求嗎?在ListPager翻頁後還需要通知ButtonPager來改變狀態呢。
l 一樣的道理,ListPager也是一個觀察者。它需要觀察ButtonPager的變動。
l 注意到在ListPager和ButtonPager的ChangePage()方法中都更新了頁面的索引值,你或許不理解為什麼Label和GridView不更新呢?其實,這並沒有什麼奇怪,ButtonPager翻頁後通知ListPager更新狀態,最需要更新的狀態就是頁面索引值,用戶不是直接點擊ListPager翻頁的,當然需要更新。Label和GridVIEw中並沒有實現是因為我們並沒有實現具體的一些細節,在實際應用中這些控件保存一些狀態也不奇怪。
l 最後來看一看怎麼牽線搭橋。我們在ButtonPager的改變頁面狀態事件中注冊了四個代理,也就是說它改變狀態後需要通知四個觀察者。怎麼是四個呢?還包括它自己,從邏輯上可能難以理解,其實這是可行的重用代碼的方案。對ButtonPager來說,是點擊哪個控件翻頁的並不重要,作為主體它的責任就是通知觀察者,作為觀察者它的責任就是更新狀態或說對事件作出響應。
l 此例完整了一個四個觀察者、兩個主體的觀察者模式。你可能角色一個類型既是觀察者又是主體不可理解,其實這在現實生活中非常多的,生物鏈中的大部分生物既是觀察者又是主體,“螳螂捕蟬,黃雀在後”中的螳螂就是。
l 再談談耦合和擴展。要再增加一個下拉框分頁的分頁控件怎麼辦?無須修改原來的代碼,再寫一個DropDownPager(繼承Pager,實現Control ),並且為它的修改分頁事件和所有觀察者掛鉤就可以了。要再增加一個ListBox控件針對不同頁數顯示不同數據怎麼辦?也無須修改原來的代碼,再寫一個ListBox控件(實現Control ),實現翻頁響應的方法,並且訂閱所有Pager的翻頁事件即可。
l 注意,本例僅僅用來演示觀察者模式的結構,並沒有遵循.Net事件模型的最佳實踐。
何時采用
通過這個例子,我們就很容易理解觀察者模式的適用點了:
l 一個對象的行為引發其它多個對象的行為。前者成為主體,後者稱為觀察者。
l 為了降低耦合,不希望主體直接調用觀察者的方法,而是采用動態訂閱主體事件的方式來進行自動的連鎖響應行為。
l 為了增加靈活性,希望動態調整訂閱主體事件的觀察者,或者希望動態調整觀察者訂閱主體的事件。