1. 意圖
定義物件間的一種一對多的依賴關系,當一個物件的狀態發生變化時,所有依賴于它的物件都得到通知并被自動更新
2. 動機
假設這樣一種情況,顧客對某個特定品牌的產品非常感興趣(例如最新型號的 iPhone 手機),而該產品很快將會在商店里出售,顧客可以每天來商店看看產品是否到貨,但如果商品尚未到貨時,絕大多數來到商店的顧客都會空手而歸,另一方面,每次新產品到貨時,商店可以向所有顧客發送郵件(可能會被視為垃圾郵件),這樣,部分顧客就無需反復前往商店了,但也可能會惹惱對新產品沒有興趣的其他顧客,
我們似乎遇到了一個矛盾:要么讓顧客浪費時間檢查產品是否到貨,要么讓商店浪費資源去通知沒有需求的顧客,觀察者模式可以解決這一問題,
觀察者模式為發布者(將自身的狀態改變通知給其他物件)類添加訂閱機制,讓每個物件都能訂閱或取消訂閱發布者事件流,該機制包括:
1)一個用于存盤訂閱者(所有希望關注發布者狀態變化的其他物件)物件參考的串列成員變數;
2)幾個用于添加或洗掉該串列中訂閱者的公有方法,
這樣,無論何時發生了重要的發布者事件,它都要遍歷訂閱者并呼叫其物件的特定通知方法,在實際應用中可能會有十幾個不同的訂閱者類跟蹤著同一個發布者類的事件, 我們不希望發布者與所有這些類相耦合的,因此,所有訂閱者都必須實作同樣的介面,發布者僅通過該介面與訂閱者互動,介面中必須宣告通知方法及其引數,這樣發布者在發出通知時還能傳遞一些背景關系資料,如果在應用中存在不同型別的發布者,且希望一個訂閱者可以同時訂閱多個發布者,需要讓所有訂閱者遵循相同的介面,并在該介面中描述幾個訂閱方法(需要將發布者作為引數傳入方法中)即可,這樣訂閱者就能在不與具體發布者類耦合的情況下通過介面觀察發布者的狀態
3. 適用性
- 一個抽象模型有兩個方面,其中一個方面依賴于另一方面,將這兩者封裝在獨立的物件中,以使它們可以各自獨立的改變和復用
- 對一個物件地改變需要同時改變其它物件,而不知道具體有多少物件有待改變
- 一個物件必須通知其他物件,而它又不能假定其他物件是誰,換言之,你不希望這些物件是緊密耦合的
4. 結構
5. 效果
Observer模式允許你獨立地改變目標和觀察者
1. 目標和觀察者間地抽象耦合 一個目標所知道的僅僅是它有一系列觀察者,每個都符合抽象的Observer類的簡單介面,目標不知道任何一個觀察者屬于哪個具體的類,這樣目標和觀察者之間地耦合是抽象和最小的,
2. 支持廣播通信 不像通常的請求,目標發送的通知不需要指定它的接收者,通知被自動廣播給所有已向該目標物件登記的物件,另外,處理還是忽略一個通知取決于觀察者
3. 意外的更新 由于一個觀察者并不知道其他觀察者地存在,它可能對改變目標的最終代價一無所知
6. 代碼實作
本例中,觀察者模式在文本編輯器的物件之間建立了間接的合作關系,每當編輯器 (Editor)物件改變時,它都會通知其訂閱者, ?郵件通知監聽器 (Email-Notification-Listener)和日志開啟監聽器 (Log-Open-Listener)都將通過執行其基本行為來對這些通知做出反應,
訂閱者類不與編輯器類相耦合,且能在需要時在其他應用中復用, ?編輯器類僅依賴于抽象訂閱者介面,這樣就能允許在不改變編輯器代碼的情況下添加新的訂閱者型別,
publisher/EventManager.java: 基礎發布者
package observer.publisher; import observer.listeners.EventListener; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author GaoMing * @date 2021/7/26 - 10:01 */ public class EventManager { Map<String, List<EventListener>> listeners = new HashMap<>(); public EventManager(String... operations) { for (String operation : operations) { this.listeners.put(operation, new ArrayList<>()); } } public void subscribe(String eventType, EventListener listener) { List<EventListener> users = listeners.get(eventType); users.add(listener); } public void unsubscribe(String eventType, EventListener listener) { List<EventListener> users = listeners.get(eventType); users.remove(listener); } public void notify(String eventType, File file) { List<EventListener> users = listeners.get(eventType); for (EventListener listener : users) { listener.update(eventType, file); } } }
editor/Editor.java: 具體發布者,由其他物件追蹤
package observer.editor; import observer.publisher.EventManager; import java.io.File; /** * @author GaoMing * @date 2021/7/26 - 10:01 */ public class Editor { public EventManager events; private File file; public Editor() { this.events = new EventManager("open", "save"); } public void openFile(String filePath) { this.file = new File(filePath); events.notify("open", file); } public void saveFile() throws Exception { if (this.file != null) { events.notify("save", file); } else { throw new Exception("Please open a file first."); } } }
listeners/EventListener.java: 通用觀察者介面
package observer.listeners; import java.io.File; /** * @author GaoMing * @date 2021/7/26 - 10:02 */ public interface EventListener { void update(String eventType, File file); }
listeners/EmailNotificationListener.java: 收到通知后發送郵件
package observer.listeners; import java.io.File; /** * @author GaoMing * @date 2021/7/26 - 10:02 */ public class EmailNotificationListener implements EventListener{ private String email; public EmailNotificationListener(String email) { this.email = email; } @Override public void update(String eventType, File file) { System.out.println("Email to " + email + ": Someone has performed " + eventType + " operation with the following file: " + file.getName()); } }
listeners/LogOpenListener.java: 收到通知后在日志中記錄一條訊息
package observer.listeners; import java.io.File; /** * @author GaoMing * @date 2021/7/26 - 10:03 */ public class LogOpenListener implements EventListener{ private File log; public LogOpenListener(String fileName) { this.log = new File(fileName); } @Override public void update(String eventType, File file) { System.out.println("Save to log " + log + ": Someone has performed " + eventType + " operation with the following file: " + file.getName()); } }
Demo.java: 客戶端代碼
package observer; import observer.editor.Editor; import observer.listeners.EmailNotificationListener; import observer.listeners.LogOpenListener; /** * @author GaoMing * @date 2021/7/26 - 10:00 */ public class Demo { public static void main(String[] args) { Editor editor = new Editor(); editor.events.subscribe("open", new LogOpenListener("/path/to/log/file.txt")); editor.events.subscribe("save", new EmailNotificationListener("[email protected]")); try { editor.openFile("test.txt"); editor.saveFile(); } catch (Exception e) { e.printStackTrace(); } } }
運行結果
Save to log \path\to\log\file.txt: Someone has performed open operation with the following file: test.txt
Email to [email protected]: Someone has performed save operation with the following file: test.txt
7. 實作
1)創建目標到其觀察者之間的映射 一個目標物件跟蹤它應通知的觀察者的最簡單的方法是顯式地在目標中保存對它們地參考,然而,當目標很多而觀察者較少時,這樣存盤可能代價太高,一個解決辦法是用時間換空間,用一個關聯查找機制(例如一個hash表)來維護目標到觀察者地映射,這樣一個沒有觀察者的目標就不產生存盤開銷,但另一方面,這一方法增加了訪問觀察者的開銷
2)觀察多個目標 有時候,一個觀察者依賴于多個目標,例如,一個表格物件可能依賴于多個資料源,在這種情況下,必須擴展update介面,目標物件可以簡單的將自己作為Update操作地一個引數,讓觀察者知道應該去檢查哪個目標
3)誰觸發更新 目標和它的觀察者依賴于通知機制來保持一致,但到底哪個物件呼叫Notify來觸發更新? 這里有兩個選擇:
- 由目標物件的狀態設定操作在改變目標物件的狀態后自動呼叫Notify,這種方法的優點是客戶不需要記住要在目標物件上呼叫Notify,缺點是多個連續的操作會產生多次連續的更新,可能效率較低
- 讓客戶負責在合適的時候呼叫notify,這樣做的優點是客戶在一系列狀態改變完成后一次性的觸發更新,避免了不必要的中間更新,缺點是給客戶增加了觸發更新的責任,由于客戶可能會忘記呼叫Notify,這種方式交易出錯
4)在發出通知前確保目標的狀態自身是一致的 在發出通知前確保狀態自身一致這一點很重要,因為觀察者在更新其狀態的程序中需要查詢目標的當前狀態,可以使用模板方法發送通知來避免這種錯誤,定義那些子類可以重定義的原語操作,并將Notify作為模板方法中的最后一個操作,這樣當子類重定義Subject的操作時,還可以保證該物件的狀態是自身一致的,另外,最好在檔案中注明哪個Subject操作觸發通知
5)避免特定于觀察者的更新協議——推/拉模型 觀察者模式的實作經常需要讓目標廣播關于其改變的其他一些資訊,目標將這些資訊作為Update操作的一個引數傳遞出去,一個極端情況是,目標向觀察者發送關于改變的詳細資訊,而不管它們需要與否,即推模型,另一個極端是拉模型,目標除最小通知外什么也不送出,而在此之后由觀察者顯式的向目標詢問細節,拉模型強調的是目標不知道它的觀察者,而推模型假定目標知道一些觀察者需要的資訊,推模型使得觀察者相對難以復用,另一方面,拉模型效率會較差,因為觀察者物件需要在沒有目標物件的幫助下,確定什么改變了
6)顯式地指定感興趣的改變 可以通過擴展目標的注冊介面,讓觀察者注冊為僅對特定事件感興趣的觀察者,如上面例子中,將EventType作為引數傳遞給Notify 和Update方法
7)封裝復雜的更新語意 當目標和觀察者間的依賴關系特別復雜時,可能需要一個維護這些關系的物件,即ChangeManager(更改管理器),其目的是盡量減少觀察者反映其目標狀態變化所需的作業量,例如,如果一個操作涉及幾個相互依賴的目標進行改動,就必須保證在所有的目標更新完畢后,才一次性的通知它們的觀察者,而不是每個目標都通知觀察者,ChangeManager是一個Mediator模式的實體,相比于,SimpleChangeManager,當一個觀察者觀察多個目標時,DAGChangeManager保證觀察者僅接收一個更新
8. 與其他模式的關系
-
責任鏈模式、命令模式、中介者模式和觀察者模式用于處理請求發送者和接收者之間的不同連接方式:
責任鏈按照順序將請求動態傳遞給一系列的潛在接收者,直至其中一名接收者對請求進行處理
命令在發送者和請求者之間建立單向連接
中介者清除了發送者和請求者之間的直接連接,強制它們通過一個中介物件進行間接溝通
觀察者允許接收者動態地訂閱或取消接收請求 -
中介者和觀察者有的時候會非常相似
中介者的主要目標是消除一系列系統組件之間的相互依賴,這些組件將依賴于同一個中介者物件,觀察者的目標是在物件之間建立動態的單向連接,使得部分物件可作為其他物件的附屬發揮作用
有一種流行的中介者模式實作方式依賴于觀察者,中介者物件擔當發布者的角色,其他組件則作為訂閱者,可以訂閱中介者的事件或取消訂閱,當中介者以這種方式實作時,它可能看上去與觀察者非常相似
9. 已知應用
觀察者模式在 Java 代碼中很常見,特別是在 GUI 組件中,它提供了在不與其他物件所屬類耦合的情況下對其事件做出反應的方式
這里是核心 Java 程式庫中該模式的一些示例:
java.util.Observer/ java.util.Observable (極少在真實世界中使用)
java.util.EventListener的所有實作 (幾乎廣泛存在于 Swing 組件中)
javax.servlet.http.HttpSessionBindingListener
javax.servlet.http.HttpSessionAttributeListener
javax.faces.event.PhaseListener
識別方法: 該模式可以通過將物件存盤在串列中的訂閱方法, 和對于面向該串列中物件的更新方法的呼叫來識別
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/308452.html
標籤:設計模式
上一篇:狀態模式(學習筆記)
下一篇:訪問者模式(學習筆記)
