這是一個原理很是重要,寫法很常見的一個模式,值得深刻理解和總結一下html
能夠想 zookeeper 等,有時系統須要定時(可插拔)接收或者監聽其餘服務的動態,這類需求常常見到,那麼觀察者模式就是作這個的:前端
一個軟件系統裏面包含了各類對象,就像一片欣欣向榮的森林充滿了各類生物同樣。在一片森林中,各類生物彼此依賴和約束,造成一個個生物鏈。一種生物的狀態變化會形成其餘一些生物的相應行動,每個生物都處於別的生物的互動之中。java
一樣,一個軟件系統經常要求在某一個對象的狀態發生變化的時候,某些其餘的對象作出相應的改變。作到這一點的設計方案有不少,可是爲了使系統可以易於複用,應該選擇低耦合度的設計方案。減小對象之間的耦合有利於系統的複用,可是同時設計師須要使這些低耦合度的對象之間可以維持行動的協調一致,保證高度的協做。編程
觀察者模式是知足這一要求的各類設計方案中最重要的一種。設計模式
通俗的解釋,聯繫生活中的郵件訂閱或者報紙,雜誌的訂閱服務安全
一、xx 報社的業務就是出版報紙服務器
二、張三向 xx 報社訂閱了 A 品牌的報紙,只要 xx 報社有新一期的 A 報紙出版,就會派快遞員給張三送到家。不只僅是張三,任何公民,只要你成爲了 xx 報社的 A 報的訂戶,你就會一直收到 A 報紙多線程
三、隨着信息時代的發展,張三迷戀起了手機新聞類的 APP,不想浪費錢訂閱紙質的 A 報紙了。此時,張三能夠取消 A 報紙的訂閱,下一期 xx 報社就不會再送新的 A 報紙給張三架構
四、只要 xx 報社還在正常營業,理論上就會一直有人(或其它單位,也就是多人)向他們訂閱報紙或取消訂閱報紙框架
這就是所謂的發佈訂閱模式的生活模型,也叫出版訂閱模式
而出版者+多個訂閱者(也能夠爲一個)= 觀察者模式,在觀察者模式裏,學術性的叫法是管出版者稱爲「主題」,訂閱者稱爲「觀察者」,僅此而已
顯然,觀察者模式定義了對象之間的一對多的依賴關係——當一個對象改變狀態時,它的全部依賴者都會受到通知並自動更新狀態。它是對象的行爲模式。
當前有一個氣象監測系統,它有三個子系統:
一、一個 WeatherData 系統,負責計算、追蹤目前的天氣情況(溫度,溼度,氣壓)。
二、三種電子顯示器(這裏不涉及前端),分別顯示給用戶目前的天氣情況、氣象統計信息、及簡單的天氣預報。當 WeatherData 從氣象站得到了最新的測量數據時,三種佈告板必須被實時更新。
三、氣象站,它是一個物理設備,能獲取實際的天氣數據。
按照OOP的通常原則,應該最好把該系統設計成一個可擴展的服務,好比:
一、好比只公佈 API,隱藏內部實現
二、讓其餘服務的 RD 能夠自定義氣象顯示器,並插入此應用中。
當前的 demo 以下:
/** * 不關心這些數據到底如何從物理設備——氣象站獲取的 * 這是硬件工程師和睦象工程師的事情 */ public class WeatherData { public int getTemperature() { return 0; } public int getHumidity() { return 0; } public int getPressure() { return 0; } public void measurementsChanged() { // 一旦氣象測量更新,此方法會被調用 } }
如上 demo 可知現狀:
一、WeatherData 類具備getter方法,能夠從氣象站取得測量值
二、當 WeatherData 從氣象站得到了最新的測量數據時,measurementsChanged()方法必需要被調用
三、須要對接的 RD 實現天氣預報的顯示功能(三個顯示器,這裏不涉及前端),即:一旦 WeatherData 獲取了新的測量數據,這些數據必須也同步更新到頁面。
另外,要求此係統必須可擴展,好比 RD 能夠自定義顯示功能,還能夠隨意的更換或增刪顯示功能,而不會影響整個系統
有 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方法 // ………… }
問題:
一、顯示器是針對具體實現編程,而非針對接口編程,面向具體的實現編程會致使咱們之後在修改顯示器的名字時,也必須修改 WeatherData 程序
二、其實第 1 點更想表達的問題是:它的可擴展性不好,若是產品須要增長新的顯示器類型,那麼每次增長(固然也包括刪除),都要打開 measurementsChanged 方法,修改代碼,在複雜的業務系統中,增長測試難度,並且反覆修改穩定的代碼容易衍生bug
三、沒法再運行時動態地增長(或刪除)顯示器
四、沒有區分變化和不變,更沒有封裝改變的部分
五、第 4 點也說明:WeatherData 類的封裝並很差
改進:
一、measurementsChanged 裏的三個 update 方法,很明顯能夠抽象出一個接口——面向接口編程
二、顯示器的實現對象們,明顯是常常須要改變的部分,應該拆分變化的部分,而且獨立抽取出來,作封裝
觀察者模式是對象的行爲模式,也叫發佈-訂閱 (Publish/Subscribe)模式、模型-視圖 (Model/View)模式(這裏能夠聯繫 MVC 架構模式)、源-監聽器 (Source/Listener) 模式或從屬者(Dependents)模式等等,其實說的都是一個東西。觀察者模式定義了一種對象間的一對多的依賴關係——讓多個觀察者對象同時監聽某一個主題對象。這個主題對象在狀態發生變化時,會通知全部已經註冊(訂閱了本身的)觀察者對象,使它們可以自動更新本身。
觀察者模式所涉及的角色有:
一、抽象主題(Subject)角色
抽象主題角色把全部對觀察者對象的引用(註冊的觀察者們,訂閱者們)保存在一個彙集(好比ArrayList對象)裏,每一個主題(其實主題也是能夠有多個的)均可以有任何數量的觀察者。抽象主題提供一個接口,能夠增長和刪除觀察者對象,抽象主題角色又叫作抽象被觀察者(Observable)角色。
二、具體主題(ConcreteSubject)角色
將有關狀態存入具體觀察者對象。在具體主題的內部狀態改變時,給全部登記(註冊)過的觀察者發出通知(notify方法調用)。具體主題角色又叫作具體被觀察者(Concrete Observable)角色。
三、抽象觀察者(Observer)角色
爲全部的具體觀察者定義一個接口,實現 update 行爲,在獲得主題的通知(notify調用)時,update 被調用,從而可以更新本身,這個接口叫作更新接口。
四、具體觀察者(ConcreteObserver)角色
存儲與主題的狀態自恰的狀態。具體觀察者角色實現抽象觀察者角色所要求的更新接口,以便使自己的狀態與主題的狀態協調。若是須要,具體觀察者角色能夠保持一個指向具體主題對象的引用。
所謂的對象間的一對多關係,是指主題是一個具備狀態的主題,這個狀態能夠改變,另外一方面,觀察者們須要使用這個變化的狀態,可是這個狀態並不屬於觀察者本身維護,那麼就須要觀察者們去依賴主題的通知,讓主題來告訴它們,什麼時候狀態發生了改變……
這就產生了一個關係——一個主題對應了多個觀察者的關係。
由於主題對象是真正的維護變化的狀態的一方,觀察者是主題的依賴方,在主題的狀態變化時,推送本身的變化給這些觀察者們,比起讓每一個觀察者自行維護該狀態(通常是一個對象)要更加安全和 OO。
在 遍歷「容器」的優雅方法——總結迭代器模式 中,闡述了高內聚和單一職責,如今看下低耦合,也叫鬆耦合設計原則。
低耦合的定義:兩個對象之間鬆耦合,就是說他們依然能夠互交,可是不清楚彼此的實現細節。
觀察者模式偏偏能提供一種一對多對象依賴關係的設計,讓主題和觀察者之間鬆耦合。
一、主題只知道各個註冊的觀察者們實現了某個接口(也就是 Observer 接口,觀察者的接口),主題不須要,也不該該知道各個觀察者的具體實現類是誰,它們都作了些什麼
二、任什麼時候候,RD 均可覺得系統增長新的觀察者。由於主題惟一依賴的東西是一個實現了 Observer 接口的對象列表,因此 RD 能夠隨時增長觀察者。一樣的,也能夠在任什麼時候候刪除某些觀察者。
三、在運行時,能夠用新的觀察者實現取代現有的觀察者實現,而主題不會受到任何影響——代碼不須要修改,假如之後擴展了新的業務,須要增長一個新的業務對象作爲觀察者,RD 不須要爲了兼容新類型而修改主題的代碼,全部要作的工做就是讓新的業務類實現觀察者接口——Observer ,並向主題註冊爲觀察者便可,主題不 care 向其註冊的對象具體都是誰,它只 care 什麼時候發送什麼通知,給全部實現了觀察者接口的對象。
低耦合的設計可以創建有彈性的OO系統,將對象間的依賴降到最低,較容易的應對變化
氣象觀測系統的 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的同時,就把變化顯示,是合理的。可是還有更好的設計方式,好比 MVC 架構模式中的實現,再下一版中說明。
若是不寫這個主題引用,那麼之後想增長取消訂閱的方法(或者其餘可能的方法),就不太方便。故仍是一開始就保留引用。
當前的實現是基於推模型的觀察者模式實現,即主題主動推送更新給觀察者,這樣作的理由:
一、主題能夠集齊全部數據,靈活的決定發送的數據量,能夠一次性的推送完整的數據給觀察者
二、觀察者不須要主動的反覆拉取數據,責任被分割
可是,有時候也得結合業務來看,好比當觀察者不少不少的時候:
一、主題也許並不能徹底掌握每一個觀察者的需求,那麼讓觀察者主動 pull 數據,也許是比較好的實現。
二、在極多個觀察者的場景下,若是僅僅是某些個別的觀察者須要一點兒數據,那麼主題仍然會通知所有的觀察者,致使和該業務無關的觀察者都要被通知,這是沒有必要的。
三、外一之後觀察者須要擴展一些狀態,若是採用推模型,那麼主題除了要新增必要的狀態屬性外,還要修改通知的代碼邏輯。若是基於拉模型,主題只須要提供一些對外的getter方法,讓觀察者調用(主動拉取數據),那麼當觀察者擴展狀態屬性時,主題就不須要修改對各個觀察者的調用代碼。僅僅增長屬性和對應的getter方法便可。
不過,生產環境中,也有不少是兩個模型都實現了。
針對 pull 模型,JDK 中已經實現的觀察者模式 API 也給咱們實現好了,也就是說,JDK 有自帶的觀察者模式 API,且能夠實現 push 或者 pull 模型的觀察者模式。
使用 JDK 內置的觀察者模式 API 實現,java.util 包內含有最基本的 Observer 接口——觀察者,與 Observable 類(注意,JDK 設計的是類)——主題,這和第 4 節中的 Subject 接口與 Observer 接口十分類似。
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() 方法。
當須要發出通知時:
一、調用 Obsecable 的 setChanged 方法,會修改一個狀態位爲 true,說明狀態已經改變了
二、調用 notifyObservers(); 或者 notifyObservers(Object obj); 前者是拉模型,後者是推模型
三、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); } } }
一、偵聽事件驅動程序設計中的外部事件
二、偵聽/監視某個對象的狀態變化
三、發佈者/訂閱者(publisher/subscriber)模型中,當一個外部事件(新的產品,消息的出現等等)被觸發時,通知郵件列表中的訂閱者
……
最經典的就是SpringMVC了,由於 MVC 架構模式就應用了觀察者模式——多個 view 註冊監聽 model。
另外,好比 Tomcat、Netty 等著名的開源軟件,也都普遍應用了觀察者模式,包括任何具備回調方法的軟件中,都有觀察者模式的思想。
很是重要的一個設計模式