前言
這是一個原理非常重要,寫法很常見的一個模式,值得深入理解和總結一下
可以想 zookeeper 等,有時系統需要定時(可插拔)接收或者監聽其他服務的動態,這類需求經常見到,那么觀察者模式就是做這個的:
一個軟件系統里面包含了各種對象,就像一片欣欣向榮的森林充滿了各種生物一樣。在一片森林中,各種生物彼此依賴和約束,形成一個個生物鏈。一種生物的狀態變化會造成其他一些生物的相應行動,每一個生物都處於別的生物的互動之中。
同樣,一個軟件系統常常要求在某一個對象的狀態發生變化的時候,某些其他的對象做出相應的改變。做到這一點的設計方案有很多,但是為了使系統能夠易於復用,應該選擇低耦合度的設計方案。減少對象之間的耦合有利於系統的復用,但是同時設計師需要使這些低耦合度的對象之間能夠維持行動的協調一致,保證高度的協作。
觀察者模式是滿足這一要求的各種設計方案中最重要的一種。
通俗的解釋,聯系生活中的郵件訂閱或者報紙,雜志的訂閱服務
1、xx 報社的業務就是出版報紙
2、張三向 xx 報社訂閱了 A 品牌的報紙,只要 xx 報社有新一期的 A 報紙出版,就會派快遞員給張三送到家。不僅僅是張三,任何公民,只要你成為了 xx 報社的 A 報的訂戶,你就會一直收到 A 報紙
3、隨着信息時代的發展,張三迷戀起了手機新聞類的 APP,不想浪費錢訂閱紙質的 A 報紙了。此時,張三可以取消 A 報紙的訂閱,下一期 xx 報社就不會再送新的 A 報紙給張三
4、只要 xx 報社還在正常營業,理論上就會一直有人(或其它單位,也就是多人)向他們訂閱報紙或取消訂閱報紙
這就是所謂的發布訂閱模式的生活模型,也叫出版訂閱模式
而出版者+多個訂閱者(也可以為一個)= 觀察者模式,在觀察者模式里,學術性的叫法是管出版者稱為“主題”,訂閱者稱為“觀察者”,僅此而已
顯然,觀察者模式定義了對象之間的一對多的依賴關系——當一個對象改變狀態時,它的所有依賴者都會受到通知並自動更新狀態。它是對象的行為模式。
案例1:氣象觀測系統
當前有一個氣象監測系統,它有三個子系統:
1、一個 WeatherData 系統,負責計算、追蹤目前的天氣狀況(溫度,濕度,氣壓)。
2、三種電子顯示器(這里不涉及前端),分別顯示給用戶目前的天氣狀況、氣象統計信息、及簡單的天氣預報。當 WeatherData 從氣象站獲得了最新的測量數據時,三種布告板必須被實時更新。
3、氣象站,它是一個物理設備,能獲取實際的天氣數據。
按照OOP的一般原則,應該最好把該系統設計成一個可擴展的服務,比如:
1、比如只公布 API,隱藏內部實現
2、讓其他服務的 RD 可以自定義氣象顯示器,並插入此應用中。
當前的 demo 如下:
/** * 不關心這些數據到底如何從物理設備——氣象站獲取的 * 這是硬件工程師和氣象工程師的事情 */ public class WeatherData { public int getTemperature() { return 0; } public int getHumidity() { return 0; } public int getPressure() { return 0; } public void measurementsChanged() { // 一旦氣象測量更新,此方法會被調用 } }
如上 demo 可知現狀:
1、WeatherData 類具有getter方法,可以從氣象站取得測量值
2、當 WeatherData 從氣象站獲得了最新的測量數據時,measurementsChanged()方法必須要被調用
3、需要對接的 RD 實現天氣預報的顯示功能(三個顯示器,這里不涉及前端),即:一旦 WeatherData 獲取了新的測量數據,這些數據必須也同步更新到頁面。
另外,要求此系統必須可擴展,比如 RD 可以自定義顯示功能,還可以隨意的更換或增刪顯示功能,而不會影響整個系統
氣象觀測系統的實現版本 1
有 RD 是這樣實現WeatherData 類的 measurementsChanged 方法的:
/** * 不關心這些數據到底如何從物理設備——氣象站獲取的 * 這是硬件工程師和氣象工程師的事情 */ public class WeatherData { // 這些方法實現,不屬於我們管 public float getTemperature() { return 0; } // 這些方法實現,不屬於我們管 public float getHumidity() { return 0; } // 這些方法實現,不屬於我們管 public float getPressure() { return 0; } public void measurementsChanged() { float temp = getTemperature(); float humidity = getHumidity(); float pressure = getPressure(); // 三種顯示器的實現類的對象: // currentConditionsDisplay 當前天氣狀態實時顯示 // statisticsDisplay 天氣數據統計信息展示 // forecastDisplay 天氣預報展示 currentConditionsDisplay.update(temp, humidity, pressure); statisticsDisplay.update(temp, humidity, pressure); forecastDisplay.update(temp, humidity, pressure); } // 這里是其他WeatherData方法 // ………… }
挑出問題
問題:
1、顯示器是針對具體實現編程,而非針對接口編程,面向具體的實現編程會導致我們以后在修改顯示器的名字時,也必須修改 WeatherData 程序
2、其實第 1 點更想表達的問題是:它的可擴展性很差,如果產品需要增加新的顯示器類型,那么每次增加(當然也包括刪除),都要打開 measurementsChanged 方法,修改代碼,在復雜的業務系統中,增加測試難度,而且反復修改穩定的代碼容易衍生bug
3、無法再運行時動態地增加(或刪除)顯示器
4、沒有區分變化和不變,更沒有封裝改變的部分
5、第 4 點也說明:WeatherData 類的封裝並不好
改進:
1、measurementsChanged 里的三個 update 方法,很明顯可以抽象出一個接口——面向接口編程
2、顯示器的實現對象們,明顯是經常需要改變的部分,應該拆分變化的部分,並且獨立抽取出來,做封裝
觀察者模式的標准定義
觀察者模式是對象的行為模式,也叫發布-訂閱 (Publish/Subscribe)模式、模型-視圖 (Model/View)模式(這里可以聯系 MVC 架構模式)、源-監聽器 (Source/Listener) 模式或從屬者(Dependents)模式等等,其實說的都是一個東西。觀察者模式定義了一種對象間的一對多的依賴關系——讓多個觀察者對象同時監聽某一個主題對象。這個主題對象在狀態發生變化時,會通知所有已經注冊(訂閱了自己的)觀察者對象,使它們能夠自動更新自己。
標准類圖和角色
觀察者模式所涉及的角色有:
1、抽象主題(Subject)角色
抽象主題角色把所有對觀察者對象的引用(注冊的觀察者們,訂閱者們)保存在一個聚集(比如ArrayList對象)里,每個主題(其實主題也是可以有多個的)都可以有任何數量的觀察者。抽象主題提供一個接口,可以增加和刪除觀察者對象,抽象主題角色又叫做抽象被觀察者(Observable)角色。
2、具體主題(ConcreteSubject)角色
將有關狀態存入具體觀察者對象。在具體主題的內部狀態改變時,給所有登記(注冊)過的觀察者發出通知(notify方法調用)。具體主題角色又叫做具體被觀察者(Concrete Observable)角色。
3、抽象觀察者(Observer)角色
為所有的具體觀察者定義一個接口,實現 update 行為,在得到主題的通知(notify調用)時,update 被調用,從而能夠更新自己,這個接口叫做更新接口。
4、具體觀察者(ConcreteObserver)角色
存儲與主題的狀態自恰的狀態。具體觀察者角色實現抽象觀察者角色所要求的更新接口,以便使本身的狀態與主題的狀態協調。如果需要,具體觀察者角色可以保持一個指向具體主題對象的引用。
理解一對多的關聯關系
所謂的對象間的一對多關系,是指主題是一個具有狀態的主題,這個狀態可以改變,另一方面,觀察者們需要使用這個變化的狀態,但是這個狀態並不屬於觀察者自己維護,那么就需要觀察者們去依賴主題的通知,讓主題來告訴它們,何時狀態發生了改變……
這就產生了一個關系——一個主題對應了多個觀察者的關系。
因為主題對象是真正的維護變化的狀態的一方,觀察者是主題的依賴方,在主題的狀態變化時,推送自己的變化給這些觀察者們,比起讓每個觀察者自行維護該狀態(一般是一個對象)要更加安全和 OO。
高內聚和低耦合的設計原則
在 遍歷“容器”的優雅方法——總結迭代器模式 中,闡述了高內聚和單一職責,現在看下低耦合,也叫松耦合設計原則。
低耦合的定義:兩個對象之間松耦合,就是說他們依然可以互交,但是不清楚彼此的實現細節。
觀察者模式恰恰能提供一種一對多對象依賴關系的設計,讓主題和觀察者之間松耦合。
1、主題只知道各個注冊的觀察者們實現了某個接口(也就是 Observer 接口,觀察者的接口),主題不需要,也不應該知道各個觀察者的具體實現類是誰,它們都做了些什么
2、任何時候,RD 都可以為系統增加新的觀察者。因為主題唯一依賴的東西是一個實現了 Observer 接口的對象列表,所以 RD 可以隨時增加觀察者。同樣的,也可以在任何時候刪除某些觀察者。
3、在運行時,可以用新的觀察者實現取代現有的觀察者實現,而主題不會受到任何影響——代碼不需要修改,假如以后擴展了新的業務,需要增加一個新的業務對象做為觀察者,RD 不需要為了兼容新類型而修改主題的代碼,所有要做的工作就是讓新的業務類實現觀察者接口——Observer ,並向主題注冊為觀察者即可,主題不 care 向其注冊的對象具體都是誰,它只 care 何時發送什么通知,給所有實現了觀察者接口的對象。
階段小結
低耦合的設計能夠建立有彈性的OO系統,將對象間的依賴降到最低,較容易的應對變化
氣象觀測系統的實現版本 2——基於推模型的觀察者模式
氣象觀測系統的 WeatherData 類正是觀察者模式中的“一”,“多”正是使用天氣觀測的各種顯示器對象。
/** * 主題接口 */ public interface Subject { void registerObserver(Observer o); void removeObserver(Observer o); void notifyObservers(); } ///////////////////////////// /** * 觀察者接口 */ public interface Observer { void update(float temp, float humidity, float pressure); } ///////////////////////////// /** * 顯示器的接口,因為每個顯示器都有一個展示的方法,故抽象出來,設計為接口 */ public interface DisplayElement { void display(); } ///////////////////////////// import java.util.ArrayList; import java.util.List; /** * 具體的主題——氣象觀測系統 */ public class WeatherData implements Subject { private List<Observer> observers; // 主題聚合了觀察者,多用聚合(組合)慎用繼承 private float temperature; private float humidity; private float pressure; public WeatherData() { observers = new ArrayList<>(); } public float getTemperature() { return temperature; } public float getHumidity() { return humidity; } public float getPressure() { return pressure; } @Override public void registerObserver(Observer o) { observers.add(o); // 注冊觀察者 } @Override public void removeObserver(Observer o) { observers.remove(o); } @Override public void notifyObservers() { for (Observer obs : observers) { obs.update(temperature, humidity, pressure); } } // 之前 demo 里的方法,抽取封裝了變化的 update 代碼,且面向接口編程,這里還能額外進行校驗等 private void measurementsChanged() { notifyObservers(); } // 被動接收氣象站的數據更新,或者主動抓取,這里可以實現爬蟲等功能 public void setMeasurements(float temperature, float humidity, float pressure) { this.temperature = temperature; this.humidity = humidity; this.pressure = pressure; // 一旦數據更新了,就立即同步觀察者們,這就是所謂的 push——推模型的觀察者設計模式的實現,對應的還有 // 一種基於拉模型的——pull模型的實現 measurementsChanged(); } } ///////////////////////////// /** * 各個顯示器類也是具體的觀察者們 */ public class CurrentConditionsDisplay implements DisplayElement, Observer { private float temperature; private float humidity; private Subject weatherData; // 非必須,必要的時候,可以聚合主題接口的引用(指針),指向具體的主題對象 // 實現向上轉型,面向接口編程,解耦合 public CurrentConditionsDisplay(Subject weatherData) { this.weatherData = weatherData; // 將自己(訂閱者)注冊到主題中(發布者中) weatherData.registerObserver(this); } @Override public void display() { System.out.println("Current conditions: " + temperature + "degrees and " + humidity + "is % humidity"); } @Override public void update(float temp, float humidity, float pressure) { this.temperature = temp; this.humidity = humidity; display(); } } ///////////////////////////// /** * 各個顯示器類也是具體的觀察者們 */ public class ForecastDisplay implements DisplayElement, Observer { private float currentPressure = 29.92f; private float lastPressure; private Subject weatherData; public ForecastDisplay(WeatherData weatherData) { this.weatherData = weatherData; weatherData.registerObserver(this); } @Override public void display() { System.out.print("Forecast: "); if (currentPressure > lastPressure) { System.out.println("Improving weather on the way!"); } else if (currentPressure == lastPressure) { System.out.println("More of the same"); } else if (currentPressure < lastPressure) { System.out.println("Watch out for cooler, rainy weather"); } } @Override public void update(float temp, float humidity, float pressure) { lastPressure = this.currentPressure; this.currentPressure = pressure; display(); } } ///////////////////////////// /** * 各個顯示器類也是具體的觀察者們 */ public class StatisticsDisplay implements Observer, DisplayElement { private float maxTemp = 0.0f; private float minTemp = 200; private float tempSum = 0.0f; private int numReadings; private Subject weatherData; public StatisticsDisplay(WeatherData weatherData) { this.weatherData = weatherData; weatherData.registerObserver(this); } @Override public void display() { System.out.println("Avg/Max/Min temperature = " + (tempSum / numReadings) + "/" + maxTemp + "/" + minTemp); } @Override public void update(float temp, float humidity, float pressure) { this.tempSum += temp; this.numReadings++; if (temp > this.maxTemp) { this.maxTemp = temp; } if (temp < this.minTemp) { this.minTemp = temp; } display(); } } ///////////////////////////// 測試類 /** * 氣象站,模擬物理設備 */ public class WeatherStation { public static void main(String[] args) { WeatherData weatherData = new WeatherData(); // 主題 // 各個觀察者注冊到主題 Observer currentDisplay = new CurrentConditionsDisplay(weatherData); Observer statisticsDisplay = new StatisticsDisplay(weatherData); Observer forecastDisplay = new ForecastDisplay(weatherData); // 本設備會給氣象觀測系統推送變化的數據 weatherData.setMeasurements(80, 65, 30.4f); weatherData.setMeasurements(82, 70, 29.2f); weatherData.setMeasurements(78, 90, 29.2f); } }
問題發現
雖然基於觀察者模式實現了該系統,但是還有不完美的地方:
要更新的數據的傳遞方式
在觀察者接口的 update 方法中,其參數是各個經常變化的,被觀測的氣象參數,我們把觀測值直接傳入觀察者中,並不是一種好的實現方法。比如,這些觀測值的種類和個數在未來有可能改變,如果以后會改變,這些變化並沒有被很好地封裝。會牽一發動全身——需要修改許多地方的代碼,再下一版中修改。
update 和 display 方法的位置
乍一看,update的同時,就把變化顯示,是合理的。但是還有更好的設計方式,比如 MVC 架構模式中的實現,再下一版中說明。
具體觀察者不寫主題的引用的后果
如果不寫這個主題引用,那么以后想增加取消訂閱的方法(或者其他可能的方法),就不太方便。故還是一開始就保留引用。
push 模型
當前的實現是基於推模型的觀察者模式實現,即主題主動推送更新給觀察者,這樣做的理由:
1、主題可以集齊所有數據,靈活的決定發送的數據量,可以一次性的推送完整的數據給觀察者
2、觀察者不需要主動的反復拉取數據,責任被分割
但是,有時候也得結合業務來看,比如當觀察者很多很多的時候:
1、主題也許並不能完全掌握每個觀察者的需求,那么讓觀察者主動 pull 數據,也許是比較好的實現。
2、在極多個觀察者的場景下,如果僅僅是某些個別的觀察者需要一點兒數據,那么主題仍然會通知全部的觀察者,導致和該業務無關的觀察者都要被通知,這是沒有必要的。
3、外一以后觀察者需要擴展一些狀態,如果采用推模型,那么主題除了要新增必要的狀態屬性外,還要修改通知的代碼邏輯。如果基於拉模型,主題只需要提供一些對外的getter方法,讓觀察者調用(主動拉取數據),那么當觀察者擴展狀態屬性時,主題就不需要修改對各個觀察者的調用代碼。僅僅增加屬性和對應的getter方法即可。
不過,生產環境中,也有很多是兩個模型都實現了。
針對 pull 模型,JDK 中已經實現的觀察者模式 API 也給我們實現好了,也就是說,JDK 有自帶的觀察者模式 API,且可以實現 push 或者 pull 模型的觀察者模式。
氣象監測系統的實現版本3——基於拉模型
使用 JDK 內置的觀察者模式 API 實現,java.util 包內含有最基本的 Observer 接口——觀察者,與 Observable 類(注意,JDK 設計的是類)——主題,這和第 4 節中的 Subject 接口與 Observer 接口十分相似。
基於 JDK 內置支持,實現觀察者模式的拉模型
WeatherData 直接擴展 java.util.Observable 類,並繼承到一些增加、刪除、通知觀察者的方法等,就搖身一變成了主題類。
各個觀察者只需要實現觀察者接口——java.uitl.Observer。
import java.util.Observable; public class WeatherData extends Observable { private float temperature; private float humidity; private float pressure; public WeatherData() { } public float getTemperature() { return temperature; } public float getHumidity() { return humidity; } public float getPressure() { return pressure; } private void measurementsChanged() { // 從java.util.Observable;繼承,線程安全 // 拉模型實現,只是設置一個狀態,如果狀態位變了,就說明數據變更 setChanged(); // 從java.util.Observable;繼承,線程安全,只有當狀態位為true,通知才有效,之后觀察者們會主動拉取數據 notifyObservers(); } public void setMeasurements(float temperature, float humidity, float pressure) { this.temperature = temperature; this.humidity = humidity; this.pressure = pressure; measurementsChanged(); } } /////////////////// 省略 DisplayElement 接口,和之前一樣 import java.util.Observable; import java.util.Observer; public class CurrentConditionsDisplay implements Observer, DisplayElement { private Observable observable; // java.util.Observable; private float temperature; private float humidity; public CurrentConditionsDisplay(Observable observable) { this.observable = observable; // 繼承自 java.util.Observable; this.observable.addObserver(this); } public void removeObserver() { this.observable.deleteObserver(this); } @Override public void display() { System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity"); } // 從 java.util.Observer; 實現來的,實現的拉模型,arg 是空 // 額外的多了 Observable o 參數,讓觀察者能知道:到底是哪個主題通知的 @Override public void update(Observable o, Object arg) { // 非常靈活的設計,可以指定觀察者只響應特定主題的通知,而不是默認強制的全部被通知 if (o instanceof WeatherData) { WeatherData weatherData = (WeatherData) o; // 拉數據,讓觀察者具有了很大靈活性——自己決定需要什么數據 this.temperature = weatherData.getTemperature(); this.humidity = weatherData.getHumidity(); display(); } } }
注冊觀察者就是調用 Obsecable 的 addObserver()方法。取消注冊,即調用 deleteObserver() 方法。
當需要發出通知時:
1、調用 Obsecable 的 setChanged 方法,會修改一個狀態位為 true,說明狀態已經改變了
2、調用 notifyObservers(); 或者 notifyObservers(Object obj); 前者是拉模型,后者是推模型
3、notifyObservers(Object obj); 是把變化的數據整合為了一個對象,在自定義實現的時候,如果 notify 的參數較多,也可以這樣封裝
關於 JDK 的觀察者模式實現,參考:JDK 自帶的觀察者模式源碼分析以及和自定義實現的取舍
回調機制和觀察者模式
所謂回調:A類調用B類的方法C,然后B類反過來調用A類的方法D,D方法就叫回調方法,和觀察者模式原理是一樣的,只不過觀察者只有一個,即一對一的關系。而且結合多線程,可以實現異步回調。
一般寫程序是你調用系統的API,如果把關系反過來,你寫一個函數,讓系統調用你的函數,那就是回調了,那個被系統調用的函數就是回調函數。
示例代碼
/** * 一請求一線程模式 */ public class ClientHandler { private final Socket clientSocket; private final ClientReadHandler clientReadHandler; private final CloseNotify closeNotify; // 回調接口 public ClientHandler(Socket clientSocket, CloseNotify closeNotify) throws IOException { this.clientSocket = clientSocket; this.clientReadHandler = new ClientReadHandler(clientSocket.getInputStream()); this.closeNotify = closeNotify; // 回調 } private void exitByMyself() { // 一些關閉的業務代碼 closeNotify.onSelfClosed(this); } /** * 回調接口:告訴服務器,某個客戶端已經退出了,服務器要刪除這個 client handler */ public interface CloseNotify { void onSelfClosed(ClientHandler thisHandler); } private class ClientReadHandler extends Thread { private final InputStream inputStream; private boolean done = false; ClientReadHandler(InputStream inputStream) { this.inputStream = inputStream; } @Override public void run() { try { InputStreamReader inputStreamReader = new InputStreamReader(ClientReadHandler.this.inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); do { String line = bufferedReader.readLine(); if (line == null) { ClientHandler.this.exitByMyself(); // line 收到的是 null,就認為異常了,需要通知服務端去除該異常客戶端 break; } System.out.println(line); } while (!this.done); } catch (Exception e) { if (!this.done) { ClientHandler.this.exitByMyself(); } } } } }
服務器端,需要實現回調接口,這里是使用的內部接口
public class TCPServer { private final int portServer; private final List<ClientHandler> clientHandlerList = new ArrayList<>(); // 保存連接的客戶端實例 private ClientListener clientListener; public TCPServer(int portServer) { this.portServer = portServer; } public boolean start(String name) { try { ClientListener clientListener = new ClientListener(name, this.portServer); this.clientListener = clientListener; clientListener.start(); } catch (Exception e) { return false; } return true; } private class ClientListener extends Thread { private ServerSocket serverSocket; private boolean done = false; ClientListener(String name, int portServer) throws IOException { super(name); this.serverSocket = create(portServer); // 監聽 portServer 端口 } @Override public void run() { int count = 0; do { Socket clientSocket; try { clientSocket = this.serverSocket.accept(); } catch (IOException e) { if (!this.done) { e.printStackTrace(); } continue; } finally { count++; } ClientHandler clientHandler; try { clientHandler = new ClientHandler(clientSocket, new ClientHandler.CloseNotify() { @Override public void onSelfClosed(ClientHandler thisHandler) { TCPServer.this.clientHandlerList.remove(thisHandler); } }); clientHandler.readAndPrint(); TCPServer.this.clientHandlerList.add(clientHandler); } catch (IOException e) { e.printStackTrace(); } } while (!this.done); } } }
觀察者模式的典型應用
1、偵聽事件驅動程序設計中的外部事件
2、偵聽/監視某個對象的狀態變化
3、發布者/訂閱者(publisher/subscriber)模型中,當一個外部事件(新的產品,消息的出現等等)被觸發時,通知郵件列表中的訂閱者
……
開源框架中應用了觀察者模式的例子
最經典的就是SpringMVC了,因為 MVC 架構模式就應用了觀察者模式——多個 view 注冊監聽 model。
另外,比如 Tomcat、Netty 等著名的開源軟件,也都廣泛應用了觀察者模式,包括任何具有回調方法的軟件中,都有觀察者模式的思想。
非常重要的一個設計模式
歡迎關注
dashuai的博客是終身學習踐行者,大廠程序員,且專注於工作經驗、學習筆記的分享和日常吐槽,包括但不限於互聯網行業,附帶分享一些PDF電子書,資料,幫忙內推,歡迎拍磚!