概述
我們可以發現這樣一個場景:如果你訂閱了一份雜志或報紙, 那就不需要再去報攤查詢新出版的刊物了。
- 出版社 (即應用中的 “發布者(publisher)”) 會在刊物出版后 (甚至提前) 直接將最新一期寄送至你的郵箱中。
出版社負責維護訂閱者(subscribers)列表, 了解訂閱者對哪些刊物感興趣。
- 當訂閱者希望出版社停止寄送新一期的雜志時, 他們可隨時從該列表中退出。
觀察者模式(Observer)是一種行為設計模式, 允許你定義一種訂閱機制, 可在對象事件發生時通知多個 “觀察” 該對象的其他對象。
- 擁有一些值得關注的狀態的對象通常被稱為
目標
, - 由于它要將自身的狀態改變通知給其他對象, 我們也將其稱為
發布者 (publisher)
。 - 所有希望關注發布者狀態變化的其他對象被稱為
訂閱者 (subscribers)
。
結構如下
其具體的結構如下
場景選擇
當一個對象狀態的改變需要改變其他對象, 或實際對象是事先未知的或動態變化的時, 可使用觀察者模式。
- 當你使用圖形用戶界面類時通常會遇到一個問題。
比如, 你創建了自定義按鈕類并允許客戶端在按鈕中注入自定義代碼, 這樣當用戶按下按鈕時就會觸發這些代碼。
- 觀察者模式允許任何實現了訂閱者接口的對象訂閱發布者對象的事件通知。
可在按鈕中添加訂閱機制, 允許客戶端通過自定義訂閱類注入自定義代碼。
當應用中的一些對象必須觀察其他對象時, 可使用該模式。 但僅能在有限時間內或特定情況下使用
。
- 訂閱列表是動態的, 因此訂閱者可隨時加入或離開該列表。
目前代碼中的實際應用
應用示例: 觀察者模式在 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
識別方法加粗樣式: 該模式可以通過將對象存儲在列表中的訂閱方法, 和對于面向該列表中對象的更新方法的調用來識別
。
偽代碼實現
發布者 (Publisher)
發布者 (Publisher) 會向其他對象發送值得關注的事件。
- 事件會在
發布者自身狀態改變或執行特定行為后發生
。 - 發布者中
包含一個允許新訂閱者加入和當前訂閱者離開列表的訂閱構架
。 - 當
新事件發生時, 發送者會遍歷訂閱列表并調用每個訂閱者對象的通知方法。 該方法是在訂閱者接口中聲明的
。
示例代碼
// 保存所有訂閱此主題的觀察者,觀察者的 數量是任意的。
// 定義 添加觀察者 (Attach) 和 刪除觀察者 (Detach) 的接口。
abstract class Publisher {protected String name;protected String state;protected List<Subscriber> subscribers = new ArrayList<Subscriber>();public abstract String getState();public abstract void setState(String state);// 發布public abstract void Notify();public Publisher(String name) {this.name = name;}// 添加觀察者public void Attach(Subscriber subscriber) {observers.add(observer);}// 刪除觀察者public void Detach(Subscriber subscriber) {observers.remove(observer);}
}
訂閱者 (Subscriber)
訂閱者 (Subscriber) 接口聲明了通知接口
。
- 在絕大多數情況下, 該接口僅包含一個 update更新方法。
- 該方法可以擁有多個參數, 使發布者能在更新時傳遞事件的詳細信息。
示例代碼
// 觀察者類,定義更新接口 (Update),當收到 Subject 的通知時,Observer 需要同步更新信息
abstract class Subscriber {protected String name;// 監聽的發布者protected Publisher publisher;public Subscriber(String name, Publisher publisher) {this.name = name;this.publisher= publisher;}public abstract void Update();
}
上下文信息
訂閱者通常需要一些上下文信息來正確地處理更新。
- 因此, 發布者通常會將一些上下文數據作為通知方法的參數進行傳遞。
- 發布者也可將自身作為參數進行傳遞, 使訂閱者直接獲取所需的數據。
如上示例所示,上下文信息便是status和name
具體發布者 (Concrete Publisher)
具體發布者 (Concrete Publisher)相當于訂閱者需要監聽的類型(因為這個類型有很多種,大部分情況下不可能只有一種發布者)
示例代碼
class ConcretePublisher extends Publisher{public ConcretePublisher(String name) {super(name);}@Overridepublic String getState() {return state;}@Overridepublic void setState(String state) {this.state = state;}@Overridepublic void Notify() {System.out.println("======= " + this.name + "主題發布新消息 =======");for (Subscriber subscriber : subscribers) {subscriber.Update();}}
}
具體訂閱者類(Concrete Subscriber )
具體訂閱者 (Concrete Subscribers) 可以執行一些操作來回應發布者的通知。
- 所有具體訂閱者類都實現了同樣的接口, 因此發布者不需要與具體類相耦合。
ConcreteSubscriber : 具體訂閱者類,實現 Subscriber 的更新接口 (Update),以便和 Publisher 同步狀態信息
示例代碼
class ConcreteSubscriber extends Subscriber {private String state;public ConcretePublisher(String name, Publisher publisher) {super(name, publisher);}@Overridepublic void Update() {state = subject.getState();System.out.println(this.name + "收到當前狀態:" + state);}
}
客戶端 (Client)
客戶端 (Client) 會分別創建發布者和訂閱者對象, 然后為訂閱者注冊發布者更新
示例代碼
public class ObserverPattern {public static void main(String[] args) {ConcretePublisher concretePublisher = new ConcretePublisher("天氣");ConcreteSubscriber sub1 = new ConcreteSubscriber("張三", concretePublisher);ConcreteSubscriber sub2= new ConcreteSubscriber("李四", concretePublisher);ConcreteSubscriber sub3= new ConcreteSubscriber("王五", concretePublisher);concretePublisher.Attach(sub1);concretePublisher.Attach(sub2);concretePublisher.Attach(sub3);concretePublisher.setState("今天下雨");concretePublisher.Notify();concretePublisher.Detach(sub2);concretePublisher.setState("明天天晴");concretePublisher.Notify();}
}
/*
======= 天氣主題發布新消息 =======
張三收到當前狀態:今天下雨
李四收到當前狀態:今天下雨
王五收到當前狀態:今天下雨
======= 天氣主題發布新消息 =======
張三收到當前狀態:明天天晴
王五收到當前狀態:明天天晴*/
實現方式
下面總結一下實現的步驟,大致可分為下面的幾步
-
仔細檢查業務邏輯, 試著將其拆分為兩個部分:
- 獨立于其他代碼的核心功能將作為發布者;
- 其他代碼則將轉化為一組訂閱類。
-
聲明訂閱者接口。 該接口至少應聲明一個 update方法。
-
聲明發布者接口并定義一些接口來在列表中添加和刪除訂閱對象。
記住發布者僅通過訂閱者接口與它們進行交互
。
-
確定存放實際訂閱列表的位置并實現訂閱方法。
- 通常所有類型的發布者代碼看上去都一樣, 因此將列表放置在直接擴展自發布者接口的抽象類中是顯而易見的。 具體發布者會擴展該類從而繼承所有的訂閱行為。
- 但是, 如果你需要在現有的類層次結構中應用該模式, 則可以考慮使用組合的方式: 將訂閱邏輯放入一個獨立的對象, 然后讓所有實際訂閱者使用該對象。
-
創建具體發布者類。 每次發布者發生了重要事件時都必須通知所有的訂閱者。
-
在具體訂閱者類中實現通知更新的方法。
- 絕大部分訂閱者需要一些與事件相關的上下文數據。 這些數據可作為通知方法的參數來傳遞。
- 但還有另一種選擇。 訂閱者接收到通知后直接從通知中獲取所有數據。 在這種情況下, 發布者必須通過更新方法將自身傳遞出去。 另一種不太靈活的方式是通過構造函數將發布者與訂閱者永久性地連接起來。
-
客戶端必須生成所需的全部訂閱者, 并在相應的發布者處完成注冊工作。