意圖
定義對象間的一種一對多的依賴關系,當一個對象的狀態發生改變時, 所有依賴於它的對象都得到通知並被自動更新。
場景
這次不說游戲了,假設我們需要在一個Web頁面上分頁顯示數據。首先需要一個分頁控制器和一個顯示數據的表格。開始,客戶的需求很簡單,需要兩個向前翻頁向後翻頁的按鈕作為控制器,還需要一個GridView來顯示數據。你可能會這麼做:
l 在頁面上放兩個按鈕和一個GridView控件
l 點擊了下一頁按鈕判斷是否超出了頁面索引,如果沒有的話更新GridView中的數據,然後更新控件的當前頁,如果翻頁後是最後一頁的話把下一頁按鈕設置為不可用。
l 點擊了上一頁按鈕判斷是否超出了頁面索引,如果沒有的話更新GridView中的數據,然後更新控件的當前頁,如果翻頁後是第一頁的話把上一頁按鈕設置為不可用。
在這裡,我們的翻頁控件僅僅和GridView進行依賴,看似問題不大。沒有想到,客戶看了Demo後覺得這樣的體驗不好,希望在頁面上呈現當前頁和總共頁。於是,我們又在頁面上加了一個Label控件,在按鈕的點擊事件裡面再去更新Label控件的值。客戶挺滿意的,隨著軟件中數據越來越多,總頁數達到了幾十頁,客戶覺得前後翻頁太不合理的了,希望有一個顯示頁數列表的分頁控制器,客戶的這個請求徹底使我們暈了,代碼被我們修改的非常混亂:
l 點擊了列表分頁控件的頁數後更新自身狀態、通知GridView加載數據、通知按鈕分頁控件更新自身狀態、通知Label更新頁數信息。
l 點擊了按鈕分頁控件後更新自身狀態、通知GridView加載數據、通知列表分頁控件更新自身狀態、通知Label更新頁數信息。
如果今後頁面上還需要針對分頁功能有任何修改的話,真不知道怎麼去改。由此引入觀察者模式來解決這些問題。
示例代碼
using System;
using System.Collections.Generic;
using System.Text;
namespace ObserverExample
{
class Program
{
static void Main(string[] args)
{
ButtonPager buttonPager = new ButtonPager();
ListPager listPager = new ListPager();
Control gridview = new GridView();
Control label = new Label ();
buttonPager.changePageHandler += new Pager.ChangePageHandler(buttonPager.ChangePage);
buttonPager.changePageHandler += new Pager.ChangePageHandler(gridview.ChangePage);
buttonPager.changePageHandler += new Pager.ChangePageHandler(label .ChangePage);
buttonPager.changePageHandler += new Pager.ChangePageHandler(listPager.ChangePage);
listPager.changePageHandler += new Pager.ChangePageHandler(buttonPager.ChangePage);
listPager.changePageHandler += new Pager.ChangePageHandler(gridview.ChangePage);
listPager.changePageHandler += new Pager.ChangePageHandler(label .ChangePage);
listPager.changePageHandler += new Pager.ChangePageHandler(listPager.ChangePage);
buttonPager.NextPage();
Console.WriteLine();
buttonPager.NextPage();
Console.WriteLine();
buttonPager.NextPage();
Console.WriteLine();
buttonPager.PreviousPage();
Console.WriteLine();
buttonPager.PreviousPage();
Console.WriteLine();
listPager.SelectPage(2);
Console.WriteLine();
listPager.SelectPage(1);
Console.WriteLine();
listPager.SelectPage(0);
}
}
abstract class Pager
{
protected int pageIndex = 0;
public int PageIndex
{
get { return pageIndex; }
set { pageIndex = value; }
}
protected int pageCount = 3;
public int PageCount
{
get { return pageCount; }
}
public event ChangePageHandler changePageHandler;
public delegate void ChangePageHandler(Pager sender);
protected void ChangePage()
{
if (changePageHandler != null )
changePageHandler(this);
}
}
class ButtonPager : Pager, Control
{
public void NextPage()
{
if (pageIndex < pageCount - 1)
{
Console.WriteLine("Click NextPage Button...");
pageIndex++;
ChangePage();
}
}
public void PreviousPage()
{
if (pageIndex > 0)
{
Console.WriteLine("Click PreviousPage Button...");
pageIndex--;
ChangePage();
}
}
public void ChangePage(Pager sender)
{
base.pageIndex = sender.PageIndex;
if (pageIndex > 0 && pageIndex < pageCount - 1)
Console.WriteLine("<<Previous Next>>");
else if (pageIndex == 0)
Console.WriteLine("Next>>");
else
Console.WriteLine("<<Previous");
}
}
class ListPager : Pager, Control
{
public void SelectPage(int pageIndex)
{
if (pageIndex >= 0 && pageIndex < pageCount)
{
Console.WriteLine(string.Format("Click <{0}> Link...", pageIndex + 1));
base.pageIndex = pageIndex;
ChangePage();
}
}
public void ChangePage(Pager sender)
{
base.pageIndex = sender.PageIndex;
for (int i = 1; i <= pageCount; i++)
{
if (pageIndex + 1 == i)
Console.Write(string.Format(" <{0}> ", i));
else
Console.Write(string.Format(" {0} ", i));
}
Console.WriteLine();
}
}
interface Control
{
void ChangePage(Pager sender);
}
class GridView : Control
{
public void ChangePage(Pager sender)
{
Console.WriteLine(string.Format("GridView->Show data of page {0}", sender.PageIndex + 1));
}
}
class Label : Control
{
public void ChangePage(Pager sender)
{
Console.WriteLine(string.Format("Label .Text=[{0}/{1}]", sender.PageIndex + 1, sender.PageCount));
}
}
}
代碼執行結果如下圖:
代碼說明
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 為了增加靈活性,希望動態調整訂閱主體事件的觀察者,或者希望動態調整觀察者訂閱主體的事件。
實現要點
l 抽象主體角色公開了自身的事件,可以給任意觀察者訂閱。
l 抽象觀察者角色定義了統一的處理行為,在C#中使用事件-代理模式的話,統一的處理行為並不這麼重要,有的時候甚至還會限制靈活性。由於本例的特殊原因,並沒有從這個接口中得益。
l 響應方法訂閱代理事件的操作可以在觀察者中定義也可以在外部定義,根據自己的需求決定,放在外部定義靈活性更高。
l 具體觀察者往往只需要實現響應方法即可。
l 可以有多個主體角色、多個觀察者角色交錯,也可以一個類型是兩個角色,主體也可以提供多個事件。從應用上來說觀察者模式變化是非常多的。
注意事項
l 由於這種靈活性,在觀察者訂閱事件的時候需要考慮是否會出現破壞行為?是否會出現無限循環或死鎖等問題?觀察者響應的時候是否會影響其它觀察者?
l 對於觀察者數量很多的時候使用觀察者模式並不適合,可能會造成性能問題。
l 在不能采用事件-代理方式完成觀察者模式的情況下(比如跨網絡應用等)可以考慮采用傳統的觀察者模式。