設計模式 | 觀察者模式及典型應用
本文主要內容:
- 介紹觀察者模式
- 微信公眾號的釋出/訂閱示例
- 觀察者模式總結
- 分析觀察者模式的典型應用
更多內容請訪問我的個人部落格:laijianfeng.org
關注【小旋鋒】微信公眾號,及時接收博文推送

觀察者模式
觀察者模式是設計模式中的 "超級模式",其應用隨處可見,我們以微信公眾號為例。
微信公眾號有服務號、訂閱號和企業號之分。以我的公眾號為例,我的公眾號型別是訂閱號,名稱是 "小旋鋒",專注於大資料,Java後端類技術分享。目前主要是分享學習筆記為主,儘量做到 "原創"、"高質量"、"成體系"。每當我釋出一篇博文推送,訂閱的使用者都能夠在我釋出推送之後及時接收到推送,即可方便地在手機端進行閱讀。

觀察者模式(Observer Pattern):定義物件之間的一種一對多依賴關係,使得每當一個物件狀態發生改變時,其相關依賴物件皆得到通知並被自動更新。觀察者模式是一種物件行為型模式。
觀察者模式的別名包括髮布-訂閱(Publish/Subscribe)模式、模型-檢視(Model/View)模式、源-監聽器(Source/Listener)模式或從屬者(Dependents)模式。
觀察者模式包含觀察目標和觀察者兩類物件,一個目標可以有任意數目的與之相依賴的觀察者,一旦觀察目標的狀態發生改變,所有的觀察者都將得到通知。
角色
Subject(目標):目標又稱為主題,它是指被觀察的物件。在目標中定義了一個觀察者集合,一個觀察目標可以接受任意數量的觀察者來觀察,它提供一系列方法來增加和刪除觀察者物件,同時它定義了通知方法notify()。目標類可以是介面,也可以是抽象類或具體類。
ConcreteSubject(具體目標):具體目標是目標類的子類,通常它包含有經常發生改變的資料,當它的狀態發生改變時,向它的各個觀察者發出通知;同時它還實現了在目標類中定義的抽象業務邏輯方法(如果有的話)。如果無須擴充套件目標類,則具體目標類可以省略。
Observer(觀察者):觀察者將對觀察目標的改變做出反應,觀察者一般定義為介面,該介面聲明瞭更新資料的方法update(),因此又稱為抽象觀察者。
ConcreteObserver(具體觀察者):在具體觀察者中維護一個指向具體目標物件的引用,它儲存具體觀察者的有關狀態,這些狀態需要和具體目標的狀態保持一致;它實現了在抽象觀察者Observer中定義的update()方法。通常在實現時,可以呼叫具體目標類的attach()方法將自己新增到目標類的集合中或通過detach()方法將自己從目標類的集合中刪除。
示例
首先需要一個訂閱者介面(觀察者),該介面有一個 receive
方法,用於接收公眾號推送通知
public interface Subscriber { int receive(String publisher, String articleName); } 複製程式碼
然後是一個微信客戶端(具體觀察者),實現了 receive
方法
public class WeChatClient implements Subscriber { private String username; public WeChatClient(String username) { this.username = username; } @Override public int receive(String publisher, String articleName) { // 接收到推送時的操作 System.out.println(String.format("使用者<%s> 接收到 <%s>微信公眾號 的推送,文章標題為 <%s>", username, publisher, articleName)); return 0; } } 複製程式碼
釋出者類(目標,被觀察物件),該類維護了一個訂閱者列表,實現了訂閱、取消訂閱、通知所有訂閱者等功能
public class Publisher { private List<Subscriber> subscribers; private boolean pubStatus = false; public Publisher() { subscribers = new ArrayList<Subscriber>(); } protected void subscribe(Subscriber subscriber) { this.subscribers.add(subscriber); } protected void unsubscribe(Subscriber subscriber) { if (this.subscribers.contains(subscriber)) { this.subscribers.remove(subscriber); } } protected void notifySubscribers(String publisher, String articleName) { if (this.pubStatus == false) { return; } for (Subscriber subscriber : this.subscribers) { subscriber.receive(publisher, articleName); } this.clearPubStatus(); } protected void setPubStatus() { this.pubStatus = true; } protected void clearPubStatus() { this.pubStatus = false; } } 複製程式碼
微信公眾號類(具體目標),該類提供了 publishArticles
方法,用於釋出推送,當文章釋出完畢時呼叫父類的通知所有訂閱者方法
public class WeChatAccounts extends Publisher { private String name; public WeChatAccounts(String name) { this.name = name; } public void publishArticles(String articleName, String content) { System.out.println(String.format("\n<%s>微信公眾號 釋出了一篇推送,文章名稱為 <%s>,內容為 <%s> ", this.name, articleName, content)); setPubStatus(); notifySubscribers(this.name, articleName); } } 複製程式碼
測試
public class Test { public static void main(String[] args) { WeChatAccounts accounts = new WeChatAccounts("小旋鋒"); WeChatClient user1 = new WeChatClient("張三"); WeChatClient user2 = new WeChatClient("李四"); WeChatClient user3 = new WeChatClient("王五"); accounts.subscribe(user1); accounts.subscribe(user2); accounts.subscribe(user3); accounts.publishArticles("設計模式 | 觀察者模式及典型應用", "觀察者模式的內容..."); accounts.unsubscribe(user1); accounts.publishArticles("設計模式 | 單例模式及典型應用", "單例模式的內容...."); } } 複製程式碼
結果如下,符合預期,當公眾號釋出一篇推送時,訂閱該公眾號的使用者可及時接收到推送的通知
<小旋鋒>微信公眾號 釋出了一篇推送,文章名稱為 <設計模式 | 觀察者模式及典型應用>,內容為 <觀察者模式的內容...> 使用者<張三> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 觀察者模式及典型應用> 使用者<李四> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 觀察者模式及典型應用> 使用者<王五> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 觀察者模式及典型應用> <小旋鋒>微信公眾號 釋出了一篇推送,文章名稱為 <設計模式 | 單例模式及典型應用>,內容為 <單例模式的內容....> 使用者<李四> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 單例模式及典型應用> 使用者<王五> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 單例模式及典型應用> 複製程式碼
可畫出類圖如下

藉此機會做個小推廣,歡迎大家關注我的微信公眾號哦 ^_^

觀察者模式總結
觀察者模式的 主要優點 如下:
-
觀察者模式可以實現表示層和資料邏輯層的分離,定義了穩定的訊息更新傳遞機制,並抽象了更新介面,使得可以有各種各樣不同的表示層充當具體觀察者角色。
-
觀察者模式在觀察目標和觀察者之間建立一個抽象的耦合。觀察目標只需要維持一個抽象觀察者的集合,無須瞭解其具體觀察者。由於觀察目標和觀察者沒有緊密地耦合在一起,因此它們可以屬於不同的抽象化層次。
-
觀察者模式支援廣播通訊,觀察目標會向所有已註冊的觀察者物件傳送通知,簡化了一對多系統設計的難度。
-
觀察者模式滿足 "開閉原則" 的要求,增加新的具體觀察者無須修改原有系統程式碼,在具體觀察者與觀察目標之間不存在關聯關係的情況下,增加新的觀察目標也很方便。
觀察者模式的 主要缺點 如下:
-
如果一個觀察目標物件有很多直接和間接觀察者,將所有的觀察者都通知到會花費很多時間。
-
如果在觀察者和觀察目標之間存在迴圈依賴,觀察目標會觸發它們之間進行迴圈呼叫,可能導致系統崩潰。
-
觀察者模式沒有相應的機制讓觀察者知道所觀察的目標物件是怎麼發生變化的,而僅僅只是知道觀察目標發生了變化。
適用場景:
-
一個抽象模型有兩個方面,其中一個方面依賴於另一個方面,將這兩個方面封裝在獨立的物件中使它們可以各自獨立地改變和複用。
-
一個物件的改變將導致一個或多個其他物件也發生改變,而並不知道具體有多少物件將發生改變,也不知道這些物件是誰。
-
需要在系統中建立一個觸發鏈,A物件的行為將影響B物件,B物件的行為將影響C物件……,可以使用觀察者模式建立一種鏈式觸發機制。
觀察者模式的典型應用
JDK 提供的觀察者介面
觀察者模式在Java語言中的地位非常重要。在JDK的 java.util
包中,提供了 Observable
類以及 Observer
介面,它們構成了JDK對觀察者模式的支援。
其中的 Observer
介面為觀察者,只有一個 update
方法,當觀察目標發生變化時被呼叫,其程式碼如下:
public interface Observer { void update(Observable o, Object arg); } 複製程式碼
Observable
類則為目標類,相比我們的示例中的 Publisher
類多了併發和NPE方面的考慮
public class Observable { private boolean changed = false; private Vector<Observer> obs = new Vector(); public Observable() { } // 用於註冊新的觀察者物件到向量中 public synchronized void addObserver(Observer var1) { if (var1 == null) { throw new NullPointerException(); } else { if (!this.obs.contains(var1)) { this.obs.addElement(var1); } } } // 用於刪除向量中的某一個觀察者物件 public synchronized void deleteObserver(Observer var1) { this.obs.removeElement(var1); } public void notifyObservers() { this.notifyObservers((Object)null); } // 通知方法,用於在方法內部迴圈呼叫向量中每一個觀察者的update()方法 public void notifyObservers(Object var1) { Object[] var2; synchronized(this) { if (!this.changed) { return; } var2 = this.obs.toArray(); this.clearChanged(); } for(int var3 = var2.length - 1; var3 >= 0; --var3) { ((Observer)var2[var3]).update(this, var1); } } // 用於清空向量,即刪除向量中所有觀察者物件 public synchronized void deleteObservers() { this.obs.removeAllElements(); } // 該方法被呼叫後會設定一個boolean型別的內部標記變數changed的值為true,表示觀察目標物件的狀態發生了變化 protected synchronized void setChanged() { this.changed = true; } // 用於將changed變數的值設為false,表示物件狀態不再發生改變或者已經通知了所有的觀察者物件,呼叫了它們的update()方法 protected synchronized void clearChanged() { this.changed = false; } // 返回物件狀態是否改變 public synchronized boolean hasChanged() { return this.changed; } // 返回向量中觀察者的數量 public synchronized int countObservers() { return this.obs.size(); } } 複製程式碼
我們可以使用 Observable
類以及 Observer
介面來重新實現微信公眾號示例。
增加一個通知類 WechatNotice
,用於推送通知的傳遞
@Data @AllArgsConstructor public class WechatNotice { private String publisher; private String articleName; } 複製程式碼
然後改寫 WeChatClient
和 WeChatAccounts
,分別實現JDK的 Observer
介面和繼承 Observable
類
public class WeChatClient implements Observer { private String username; public WeChatClient(String username) { this.username = username; } @Override public void update(Observable o, Object arg) { //WeChatAccounts weChatAccounts = (WeChatAccounts) o; WechatNotice notice = (WechatNotice) arg; System.out.println(String.format("使用者<%s> 接收到 <%s>微信公眾號 的推送,文章標題為 <%s>", username, notice.getPublisher(), notice.getArticleName())); } } public class WeChatAccounts extends Observable { private String name; public WeChatAccounts(String name) { this.name = name; } public void publishArticles(String articleName, String content) { System.out.println(String.format("\n<%s>微信公眾號 釋出了一篇推送,文章名稱為 <%s>,內容為 <%s> ", this.name, articleName, content)); setChanged(); notifyObservers(new WechatNotice(this.name, articleName)); } } 複製程式碼
測試,與示例中的測試程式碼的區別在於呼叫的方法不同
public class Test { public static void main(String[] args) { WeChatAccounts accounts = new WeChatAccounts("小旋鋒"); WeChatClient user1 = new WeChatClient("張三"); WeChatClient user2 = new WeChatClient("李四"); WeChatClient user3 = new WeChatClient("王五"); accounts.addObserver(user1); accounts.addObserver(user2); accounts.addObserver(user3); accounts.publishArticles("設計模式 | 觀察者模式及典型應用", "觀察者模式的內容..."); accounts.deleteObserver(user1); accounts.publishArticles("設計模式 | 單例模式及典型應用", "單例模式的內容...."); } } 複製程式碼
測試結果如下,可以發現結果如示例一致
<小旋鋒>微信公眾號 釋出了一篇推送,文章名稱為 <設計模式 | 觀察者模式及典型應用>,內容為 <觀察者模式的內容...> 使用者<王五> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 觀察者模式及典型應用> 使用者<李四> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 觀察者模式及典型應用> 使用者<張三> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 觀察者模式及典型應用> <小旋鋒>微信公眾號 釋出了一篇推送,文章名稱為 <設計模式 | 單例模式及典型應用>,內容為 <單例模式的內容....> 使用者<王五> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 單例模式及典型應用> 使用者<李四> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 單例模式及典型應用> 複製程式碼
Guava EventBus 中的觀察者模式
Guava 中的 EventBus
封裝了友好的 "生產/消費模型",通過非常簡單的方式,實現了觀察者模式中的監聽註冊,事件分發。
使用了 Guava EventBus
之後,如果需要訂閱訊息,不需要實現任何介面,只需在監聽方法上加上 @Subscribe
註解即可, EventBus
提供了 register
和 unregister
方法用於註冊與取消註冊事件,當 EventBus
呼叫 post
方法時將把事件分發給註冊的物件
使用 Guava 重新實現示例
@Data @AllArgsConstructor public class WechatNotice { private String publisher; private String articleName; } public class WeChatClient{ private String username; public WeChatClient(String username) { this.username = username; } @Subscribe public void listen(WechatNotice notice) { System.out.println(String.format("使用者<%s> 接收到 <%s>微信公眾號 的推送,文章標題為 <%s>", username, notice.getPublisher(), notice.getArticleName())); } } public class WeChatAccounts { private String name; private EventBus eventBus; public WeChatAccounts(String name) { this.name = name; this.eventBus = new EventBus(); } public void publishArticles(String articleName, String content) { System.out.println(String.format("\n<%s>微信公眾號 釋出了一篇推送,文章名稱為 <%s>,內容為 <%s> ", this.name, articleName, content)); this.eventBus.post(new WechatNotice(this.name, articleName)); } public void register(WeChatClient weChatClient) { this.eventBus.register(weChatClient); } public void unregister(WeChatClient weChatClient) { this.eventBus.unregister(weChatClient); } } 複製程式碼
測試
public class Test { public static void main(String[] args) { WeChatAccounts accounts = new WeChatAccounts("小旋鋒"); WeChatClient user1 = new WeChatClient("張三"); WeChatClient user2 = new WeChatClient("李四"); WeChatClient user3 = new WeChatClient("王五"); accounts.register(user1); accounts.register(user2); accounts.register(user3); accounts.publishArticles("設計模式 | 觀察者模式及典型應用", "觀察者模式的內容..."); accounts.unregister(user1); accounts.publishArticles("設計模式 | 單例模式及典型應用", "單例模式的內容...."); } } 複製程式碼
不出意料,輸出的內容與上面兩個示例一樣
<小旋鋒>微信公眾號 釋出了一篇推送,文章名稱為 <設計模式 | 觀察者模式及典型應用>,內容為 <觀察者模式的內容...> 使用者<張三> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 觀察者模式及典型應用> 使用者<李四> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 觀察者模式及典型應用> 使用者<王五> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 觀察者模式及典型應用> <小旋鋒>微信公眾號 釋出了一篇推送,文章名稱為 <設計模式 | 單例模式及典型應用>,內容為 <單例模式的內容....> 使用者<李四> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 單例模式及典型應用> 使用者<王五> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 單例模式及典型應用> 複製程式碼
Guava EventBus 的更多用法可自行檢視相關文件
Guava EventBus 原始碼分析可看這篇t.cn/EZzC35B
JDK 委託事件模型DEM中的觀察者模式
首先來敲一個AWT按鈕監聽事件的Demo
import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; public class MouseEvents { private Frame frame; private Button button; MouseEvents() { frame = new Frame("點選按鈕觸發點選事件,控制檯將列印日誌"); frame.setBounds(300, 200, 600, 300); frame.setLayout(new FlowLayout()); button = new Button("this is a button"); button.setFont(new Font("Default", 0, 30)); frame.add(button); dealwithEvent(); frame.setVisible(true); } //事件監聽器以及處理事件 private void dealwithEvent() { // 監聽窗體關閉事件 frame.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { System.exit(0); } }); button.addActionListener(new ActionListener() { private int eventCount = 1; @Override public void actionPerformed(ActionEvent e) { System.out.println(String.format("動作事件發生 %d 次", eventCount++)); } }); } public static void main(String[] args) { new MouseEvents(); } } 複製程式碼
執行 main 方法桌面將彈出下面的面板和按鈕

按鈕的 addActionListener
新增指定的動作偵聽器,以接收發自此按鈕的動作事件,當用戶在按鈕上按下或釋放滑鼠時,JVM將產生一個相應的 ActionEvent
型別的事件物件,並在觸發事件時將呼叫按鈕的 fireXXX()
方法(繼承自 Component),在該方法內部,將呼叫註冊到按鈕中的 ActionListener
物件的 actionPerformed()
方法(也就是我們實現的匿名事件處理類),實現對事件的處理
動作事件發生 1 次 動作事件發生 2 次 動作事件發生 3 次 動作事件發生 4 次 複製程式碼
Spring ApplicationContext 事件機制中的觀察者模式
spring的事件機制是從java的事件機制拓展而來, ApplicationContext
中事件處理是由 ApplicationEvent
類和 ApplicationListener
介面來提供的。如果一個Bean實現了 ApplicationListener
介面,並且已經發布到容器中去,每次 ApplicationContext
釋出一個 ApplicationEvent
事件,這個Bean就會接到通知
- ApplicationContext:事件源,其中的 publishEvent()方法用於觸發容器事件
- ApplicationEvent:事件本身,自定義事件需要繼承該類,可以用來傳遞資料
- ApplicationListener:事件監聽器介面,事件的業務邏輯封裝在監聽器裡面
使用 spring 事件機制重新實現示例
@Data public class WechatNotice extends ApplicationEvent { private String publisher; private String articleName; public WechatNotice(Object source, String publisher, String articleName) { super(source); this.publisher = publisher; this.articleName = articleName; } } public class WeChatClient implements ApplicationListener { private String username; public WeChatClient(String username) { this.username = username; } @Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof WechatNotice) { WechatNotice notice = (WechatNotice) event; System.out.println(String.format("使用者<%s> 接收到 <%s>微信公眾號 的推送,文章標題為 <%s>", username, notice.getPublisher(), notice.getArticleName())); } } public void setUsername(String username) { this.username = username; } } public class WeChatAccounts implements ApplicationContextAware { private ApplicationContext ctx; private String name; public WeChatAccounts(String name) { this.name = name; } public void setName(String name) { this.name = name; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.ctx = applicationContext; } public void publishArticles(String articleName, String content) { System.out.println(String.format("\n<%s>微信公眾號 釋出了一篇推送,文章名稱為 <%s>,內容為 <%s> ", this.name, articleName, content)); ctx.publishEvent(new WechatNotice(this.name, this.name, articleName)); } } 複製程式碼
在 resources 目錄下建立 spring.xml
檔案,填入下面的內容
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="WeChatAccounts" class="com.observer.sprintevent.WeChatAccounts" scope="prototype"> <constructor-arg name="name" value=""></constructor-arg> </bean> <bean id="WeChatClient1" class="com.observer.sprintevent.WeChatClient"> <constructor-arg name="username" value="張三"></constructor-arg> </bean> <bean id="WeChatClient2" class="com.observer.sprintevent.WeChatClient"> <constructor-arg name="username" value="李四"></constructor-arg> </bean> <bean id="WeChatClient3" class="com.observer.sprintevent.WeChatClient"> <constructor-arg name="username" value="王五"></constructor-arg> </bean> </beans> 複製程式碼
測試
public class Test { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml"); WeChatAccounts accounts = (WeChatAccounts) context.getBean("WeChatAccounts"); accounts.setName("小旋鋒"); accounts.setApplicationContext(context); accounts.publishArticles("設計模式 | 觀察者模式及典型應用", "觀察者模式的內容..."); } } 複製程式碼
輸出如下
<小旋鋒>微信公眾號 釋出了一篇推送,文章名稱為 <設計模式 | 觀察者模式及典型應用>,內容為 <觀察者模式的內容...> 使用者<張三> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 觀察者模式及典型應用> 使用者<李四> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 觀察者模式及典型應用> 使用者<王五> 接收到 <小旋鋒>微信公眾號 的推送,文章標題為 <設計模式 | 觀察者模式及典型應用> 複製程式碼
在此示例中 ApplicationContext
物件的實際型別為 ClassPathXmlApplicationContext
,其中的與 publishEvent
方法相關的主要程式碼如下:
private ApplicationEventMulticaster applicationEventMulticaster; public void publishEvent(ApplicationEvent event) { this.getApplicationEventMulticaster().multicastEvent(event); if (this.parent != null) { this.parent.publishEvent(event); } } ApplicationEventMulticaster getApplicationEventMulticaster() throws IllegalStateException { return this.applicationEventMulticaster; } protected void initApplicationEventMulticaster() { ConfigurableListableBeanFactory beanFactory = this.getBeanFactory(); if (beanFactory.containsLocalBean("applicationEventMulticaster")) { this.applicationEventMulticaster = (ApplicationEventMulticaster)beanFactory.getBean("applicationEventMulticaster", ApplicationEventMulticaster.class); } else { this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory); beanFactory.registerSingleton("applicationEventMulticaster", this.applicationEventMulticaster); } } 複製程式碼
其中的 SimpleApplicationEventMulticaster
如下, multicastEvent
方法主要是通過遍歷 ApplicationListener
(註冊由 AbstractApplicationEventMulticaster 實現),使用執行緒池框架 Executor
來併發執行 ApplicationListener
的 onApplicationEvent
方法,與示例本質上是一致的
public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster { private Executor taskExecutor; public void multicastEvent(final ApplicationEvent event) { Iterator var2 = this.getApplicationListeners(event).iterator(); while(var2.hasNext()) { final ApplicationListener listener = (ApplicationListener)var2.next(); Executor executor = this.getTaskExecutor(); if (executor != null) { executor.execute(new Runnable() { public void run() { listener.onApplicationEvent(event); } }); } else { listener.onApplicationEvent(event); } } } } 複製程式碼