1、意圖
定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都得到通知並被自動更新。
2、別名
依賴(Dependents),發佈-訂閱(Publish-Subscribe)
3、動機
將一個系統分割成一系列相互協作的類有一個常見的副作用:需要維護相關的對象間的一致性。我們不希望爲了維持一致性而使各類緊密耦合,因爲這樣降低了他們的可重用性。
例如,許多圖形用戶界面工具箱將用戶應用的界面表示與底下的應用數據分離。定義應用數據的類和負責界面表示的類可以各自獨立地複用。當然它們也可一起工作。一個表格對象和一個柱形圖對象可使用不同的表示形式描述同一個應用數據對象的信息。表格對象和柱形圖對象互相併不知道對方的存在,這樣使你可以根據需要單獨複用表格或柱狀圖。但在這裏使它們表現的似乎相互知道。當用戶改變表格中的信息時,柱狀圖立即反應這一變化,反過來也是如此。
這一行爲意味着表格對象和棒狀圖對象都依賴於數據對象,因此數據對象的任何狀態改變都應立即通知它們。同時也沒有理由將依賴於該數據對象的對象數目限定爲兩個,對西寧同的數據可以有任意數目的不同用戶界面。
Observer模式描述瞭如何建立這種關係。這一模式中的關鍵對象是目標(subject)和觀察者(observer)。一個目標可以有任意數目的依賴它的觀察者。一旦目標的狀態發生改變,所有的觀察者都得到通知。作爲對這個通知的相應,每個觀察者都將查詢目標以使其狀態與目標的狀態同步。
這種交互也稱爲發佈-訂閱(public-subscribe)。目標是通知的發佈者。它發出通知時並不需知道誰是它的觀察者。可以有任意數目的觀察者訂閱並接收通知。
4、適用性
在以下任一情況下可以使用觀察者模式:
· 當一個抽象模型有兩個方面,其中一方面依賴於另一方面。將這二者封裝在獨立的對象中以使它們可以各自獨立地改變和複用。
· 當對一個對象的改變需要同時改變其它對象,而不知道具體有多少對象有待改變。
· 當一個對象必須通知其它對象,而它又不能假定其它對象是誰。換言之,你不希望這些對象是緊密耦合的。
5、結構
6、參與者
· Subject(目標)
-- 目標知道它的觀察者。可以有任意多個觀察者觀察同一個目標。
-- 提供註冊和刪除觀察者對象的接口。
· Observer(觀察者)
-- 爲那些在目標發生改變時需要獲得通知的對象定義一個更新接口。
· ConcreteSubject(具體目標)
-- 將有關狀態存入各ConcreteObserver對象。
-- 當它的狀態發生改變時,向它的各個觀察者發出通知。
· ConcreteObserver(具體觀察者)
-- 維護一個指向ConcreteSubject對象的引用。
-- 存儲有關狀態,這些狀態應與目標的狀態保持一致。
-- 實現Observer的更新接口以使自身狀態與目標的狀態保持一致。
7、協作
· 當ConcreteSubject發生任何可能導致其觀察者與其本身狀態不一致的改變時,它將通知它的各個觀察者。
· 在得到一個具體目標的改變通知後,ConcreteObserver對象可向目標對象查詢信息。ConcreteObserver使用這些信息以使它的狀態與目標對象的狀態一致。
下面的交互圖說明了一個目標對象和兩個觀察者之間的協作:
注意發出改變請求的Observer對象並不立即更新,而是將其推遲到它從目標得到一個通知之後。Notify不總是由目標對象調用。它也可被一個觀察者或其它對象調用。實現一節將討論一些常用的變化。
8、效果
Observer模式允許你獨立的改變目標和觀察者。你可以單獨複用目標對象而無需同時複用其觀察者,反之亦然。它也是你可以在不改動目標和其他的觀察者的前提下增加觀察者。
下面是觀察者模式其它一些優缺點:
1)目標和觀察者間的抽象耦合 一個目標所知道的僅僅是它有一系列觀察者,每個都符合抽象的Observer類的簡單接口。目標不知道任何一個觀察者屬於哪一個具體的類。這樣目標和觀察者之間的耦合是抽象的和最小的。
因爲目標和觀察者不是緊耦合的,他們可以屬於一個系統中的不同抽象層次。一個處於較低層次的目標對象可與一個處於較高層次的觀察者通信並通知它,這樣就就保持了系統層次的完整。如果目標和觀察者混在一塊兒,那麼得到的對象要麼橫貫兩個層次(違反了層次性),要麼必須放在這兩層的某一層中(這可能會損害層次抽象)。
2) 支持廣播通信 不像通常的請求,目標發送的通知不需指定它的接收者。通知被自動廣播給所有已向該目標對象登記的有關對象。目標對象並不關心到底有多少對象對自己感興趣;它唯一的責任就是通知它的各觀察者。這給了你在任何時刻增加和刪除觀察者的自由。處理還是忽略一個通知取決於觀察者。
3) 意外的更新 因爲一個觀察者並不知道其它觀察者的存在,它可能對改變目標的最終代價一無所知。在目標上一個看似無害的操作可能會引起一系列對觀察者以及依賴於這些觀察者的那些對象的更新。此外,如果依賴準則的定義或維護不當,常常會引起錯誤的更新,這種錯誤通常很難捕捉。
簡單的更新協議不提供具體細節說明目標中什麼被改變了,這就使得上述問題更加嚴重。如果沒有其他協議幫助觀察者發現什麼發生了改變,他們可能會被迫盡力減少改變。
9、實現
這一節討論一些與實現依賴機制相關的問題。
1)創建目標到其觀察者之間的映射 一個目標對象跟蹤它應通知的觀察者的最簡單的方式是顯式地在目標中保存對它們的引用。然而,當目標很多而觀察者較少時,這樣存儲可能代價太高。一個解決辦法是用時間換空間,用一個關聯查找機制(例如一個hash表)來維護目標到觀察者的映射。這樣一個沒有觀察者的目標就不差生存儲開銷。但另一方面,這一方法增加了訪問觀察者的開銷。
2)觀察多個目標 在某些情況下,一個觀察者依賴於多個目標可能是有意義的。例如,一個表格對象可能依賴於多個數據源。在這種情況下,必須擴展Update接口以使觀察者知道是哪一個目標送來的通知。目標對象可以簡單地將自己作爲Update操作的一個參數,讓觀察者知道應去檢查哪一個目標。
3)誰觸發更新 目標和它的觀察者依賴於通知機制來保持一致。但到底哪一個對象調用Notify來觸發更新?此時有兩個選擇。
a) 由目標對象的狀態設定操作在改變目標對象的狀態後自動調用Notify。這種方法的優點是客戶不需要記住要在目標對象上調用Notify,缺點是多個連續的操作會產生多次連續的更新,可能效率較低。
b) 讓客戶負責愛適當的時候調用Notify。這樣做的優點是客戶可以在一系列的狀態改變完成後再一次性的觸發更新,避免了不必要的中間更新。確定是給客戶增加了觸發更新的責任。猶豫客戶可能會忘記調用Notify,這種方式較易出錯。
4) 對已刪除目標的懸掛引用 刪除一個目標時應注意不要在其觀察着中遺留對該目標的懸掛引用。一種避免懸掛引用的方法是,當一個目標被刪除時,讓它通知它的觀察者將對該目標的引用復位。一般來說,不能簡單地刪除觀察者,因爲其他的對象可能會引用它們,或者也可能它們還在觀察其他的目標。
5)在發出通知前確保目標的狀態自身是一致的 在發出通知前確保狀態自身一致這一點很重要,因爲觀察者在更新其狀態的過程中需要查詢目標的當前狀態。
當Subject的子類調用繼承的該項操作時,很容易無意中違反這條自身一致的準則。
6) 避免特定於更新者的更新協議----推/拉模型 觀察者模式的實現經常需要讓目標廣播關於其改變的其他一些信息。目標將這些信息作爲Update操作的一個參數傳遞出去。這些信息的量可能很小,也可能很大。
一個極端情況是,目標向觀察者發送關於改變的詳細信息,而不管他們需要與否。我們稱爲推模型。另一個極端是拉模型;目標除最小通知外什麼也不送出,而在次之後由觀察者顯式地向目標詢問細節。
拉模型強調的是目標不知道它的觀察者,而推模型假定目標知道一些觀察者的需要的信息。推模型可能使得觀察者相對難以複用,因爲目標對觀察者的假定可能並不總是正確的。另一方面。拉模型可能效率較差,因爲觀察者對象需在沒有目標對象幫助的情況下確定什麼改變了。
7) 顯式地指定感興趣的改變 你可以擴展目標的註冊接口,讓各觀察者註冊爲僅對特定事件感興趣,以提高更新的效率。當一個事件發生時,目標僅通知那些已註冊爲對該事件感興趣的觀察者。支持這種做法一種途徑是,對使用目標對象的方面(aspects)的概念。
8)封裝複雜的更新語義 當目標和觀察者間的依賴關係特別複雜時,可能需要一個維護這些關係的對象。我們稱這樣的對象爲更改管理器(ChangeManager)。它的目的是儘量減少觀察者反映其目標的狀態變化所需的工作量。例如,如果一個操作涉及到對幾個相互依賴的目標進行改動,就必須保證僅在所有的目標都已更改完畢後,才一次性的通知他們的觀察者,而不是每個目標都通知觀察者。
ChangeManager有三個責任:
a)它將一個目標映射到它的觀察者並提供一個接口來維護這個映射。這就不需要由目標來維護對其觀察者的引用,反之亦然。
b)它定義一個特定的更新策略。
c)根據一個目標的請求,它更新所有依賴於這個目標的觀察者。
9)結合目標類和觀察者類 用不支持多重繼承的語言(如Smalltalk)書寫的類庫通常不單獨定義Subject和Observer類,而是將它們的接口結合到一個類中。這就允許你定義一個既是目標又是一個觀察者的對象,而不許要多重繼承。例如在Smalltalk中,Subject和Observer接口定義於根類Object中,使得它們對所有的類都可用。
10、代碼示例
。。。。。。。。