本文參照《Head First 設計模式》,轉載請註明出處
對於整個系列,我們按照這本書的設計邏輯,使用情景分析的方式來描述,並且穿插使用一些問題,總結的方式來講述。並且所有的開發原始碼,都會託管到github上。 專案地址:github.com/jixiang5200…
前一章主要講解了設計模式入門和最常用的一個模式-----策略模式,並結合Joe的鴨子模型進行分析,想要了解的朋友可以回去回看一下。 這裡我們將繼續介紹一種可以幫助物件知悉現狀,不會錯過該物件感興趣的事。甚至物件可以自己決定是都要繼續接受通知。有過設計模式學習經驗的人會脫口而出-----觀察者模式。對的,接下來我們將瞭解一個新的設計模式,也就是觀察者模式。
1.引言
最近你的團隊獲取了一個新的合約,需要負責建立一個Weather-O-Rama公司的下一代氣象站----Internet氣象觀測站。 合約內容如下:
恭喜貴公司獲選為敝公司建立下一代Internet氣象觀測站!該氣象站必須建立在我們專利申請的WeatherData物件上,由WeatherData物件負責追蹤目前的天氣狀況(溫度、溼度、氣壓)。我們希望貴公司能建立一個應用,有三種佈告板,分別顯示目前的狀況、氣象統計及簡單的預報。當WeatherData物件獲取到最新的測量資料時,三種佈告板必須實時更新。 而且,這是一個可以擴充的氣象站,Weather-O-Rama氣象站希望公佈一組API,讓其他開發人員可以寫出自己的氣象佈告板,並插入此應用中我們希望貴公司可以提供這樣的API。 Weather-O-Rama氣象站有很好的商業運營模式:一旦客戶上鉤,他們使用每個佈告板都要付錢最好的部分就是,為了感謝貴公司建立此係統,我們將以公司的認股權支付你。 我們期待看到你的設計和應用的alpha版本。 附註:我們正在通宵整理WeatherData原始檔給你們。
1.1需求分析
根據開發的經驗,我們首先分析Weather-O-Rama公司的需求:
- 此係統有三個部分組成:氣象站(獲取實際的氣象資料的物理組成),WeatherData物件(追蹤來自氣象站的資料,並更新佈告板)和佈告板(顯示目前天氣狀況展示給使用者)
- 專案應用中,開發者需要利用WeatherData去實時獲取氣象資料,並且更新三個佈告板:目前氣象,氣象統計和天氣預報。
- 系統必須具備很高的可擴充性,讓其他的開發人員可以建立定製的佈告板,使用者可以隨心所欲地新增或刪除任何佈告板。
我們初始設計結構如下:
1.2WeatherData類
第二天,Weather-O-Rama公司傳送過來WeatherData的原始碼,其結構如下圖
其中measurementsChanged()方法在氣象測試更新時,被呼叫。
1.3錯誤的編碼方式
首先,我們從大部分不懂設計模式的開發者常用的設計方式開始。 根據Weather-O-Rama氣象站開發人員的需求暗示,在measurementsChanged()方法中新增相關的程式碼:
public class WeatherData {
private float temperature;//溫度
private float humidity;//溼度
private float pressure;//氣壓
private CurrentConditionsDisplay currentConditionsDisplay;//目前狀態佈告板
private StatisticsDisplay statisticsDisplay;//統計佈告板
private ForecastDisplay forecastDisplay;//預測佈告板
public WeatherData(CurrentConditionsDisplay currentConditionsDisplay
,StatisticsDisplay statisticsDisplay
,ForecastDisplay forecastDisplay){
this.currentConditionsDisplay=currentConditionsDisplay;
this.statisticsDisplay=statisticsDisplay;
this.forecastDisplay=forecastDisplay;
}
public float getTemperature() {
return temperature;
}
public float getHumidity(){
return humidity;
}
public float getPressure(){
return pressure;
}
//例項變數宣告
public void measurementsChanged(){
//呼叫WeatherData的三個getter方法獲取最近的測量值
float temp=getTemperature();
float humidity=getHumidity();
float pressure=getPressure();
currentConditionsDisplay.update(temp,humidity,pressure);
statisticsDisplay.update(temp,humidity,pressure);
forecastDisplay.update(temp,humidity,pressure);
}
//通知發生變化
public void setMeasurements(float temperature,float humidity,float pressure){
this.temperature=temperature;
this.humidity=humidity;
this.pressure=pressure;
measurementsChanged();
}
}
複製程式碼
回顧第一章的三個設計原則,我們發現這裡違反了幾個原則
第一設計原則 找出應用中可能需要變化之處,把它們獨立出來,不要和那些不需要變化的程式碼混合在一起。
第二設計原則 針對於介面程式設計,不針對實現程式設計
第三設計原則 多用組合,少用繼承
在這裡我們使用了針對實現程式設計,並且沒有將變化部分獨立出來,這樣會導致我們以後在增加或刪除佈告板時必須修改應用程式。而且,最重要的是,我們犧牲了可擴充性。
既然這裡我們提到了要使用觀察者模式來解決問題,那麼該如何下手。並且,什麼是觀察者模式?2.觀察者模式
2.1認識觀察者模式
為了方便理解,我們從日常生活中常遇到的情形來理解觀察者模式,這裡我們使用生活常見的報紙和雜誌訂閱業務邏輯來理解:
- 報社的業務在於出版報紙
- 訂閱報紙的使用者,只要對應報社有新的報紙出版,就會給你送來
- 當使用者不想繼續訂閱報紙,可以直接取消訂閱。那麼之後就算有新的報紙出版,也不會送給對應使用者了。
- 只要報社一直存在,任何使用者都可以自由訂閱或取消訂閱報紙
從上面的邏輯我們分析出,這裡由以下部分組成,報社,使用者,訂閱。將其抽象出來就i是:出版者,訂閱者,訂閱。這裡觀察者模式的雛形已經出來了。
出版者+訂閱者=觀察者模式
如果上面已經理解了報社報紙訂閱的邏輯,也可以很快知道觀察者模式是什麼。只是在其中名稱會有差異,前面提到的“出版者”我們可以稱為**“主題(Subject)”或“被觀察者(Observable)”(後一個更加常用),“訂閱者”我們稱為“觀察者(Observer)”**,這裡我們採用類UML的結構圖來解釋:
2.2 觀察者模式註冊/取消註冊
場景1: 某一天,鴨子物件覺得自己的朋友都訂閱了主題,自己也想稱為一個觀察者。於是告訴主題,它想當一個觀察者。完成訂閱後,鴨子也成為一個觀察者了。
這樣當主題資料發生變化時,鴨子物件也可以得到通知了!!場景2: 老鼠物件厭煩了每天都被主題煩,決定從觀察者序列離開,於是它告訴主題它想離開觀察者行列,主題將它從觀察者中除名。
之後主題資料發生變化時,不會再通知老鼠物件。上面的兩個情形分別對應了註冊和取消註冊,這也是觀察者模式最重要的兩個概念。註冊後的物件我們才可以稱為觀察者。觀察者取消註冊後也不能稱為觀察者。
2.3 觀察者模式定義
通過報紙業務和物件訂閱的例子,我們可以勾勒出觀察者模式的基本概念。
觀察者模式定義了物件之間的一對多的依賴,這樣一來,當一個物件改變狀態時,它所有的依賴者都會收到通知並自動更新。
主題/被觀察者和觀察者之間定義了一對多的關係。觀察者依賴於主題/被觀察者。一旦主題/被觀察者資料發生改變的時候,觀察者就會收到通知。那麼,如何實現觀察者和主題/被觀察者呢?
2.4 觀察者模式實現
由於網路上的實現觀察者的方式非常多,我們這裡採取比較容易理解的方式Subject和Observer。對於更高階的使用方式,可以百度。 接下來我們來看看基於Subject和Observer的類圖結構:
3. 設計氣象站
到這裡我們再回到當初的問題,氣象站中結構模型為一對多模型,其中WeatherData為氣象模型中的“一”,而“多”也就對應了這裡用來展示天氣監測資料的各種佈告板。相對於之前的針對實現的方式,使用觀察者模式來設計會更加符合需求。優先我們給出新的氣象站模型。
3.1實現氣象站
依照前面的設計結構圖,最終來實現具體程式碼結構
1.Subject
public interface Subject {
//註冊觀察者
public void registerObserver(Observer o);
//刪除觀察者
public void removeObserver(Observer o);
//當主題發生資料變化時,通知所有觀察
public void notifyObservers();
}
複製程式碼
2.Observer
public interface Observer {
/**
*
* update:當氣象站的觀測資料發生改變時,這個方法會被呼叫
* @param temp 溫度
* @param hunmidity 溼度
* @param pressure 氣壓
* @since JDK 1.6
*/
public void update(float temp,float hunmidity,float pressure);
}
複製程式碼
3.DisplayElement
public interface DisplayElement {
//當佈告板需要展示時,呼叫此方法時
public void display();
}
複製程式碼
4.新的WeatherData1
public class WeatherData1 implements Subject{
private ArrayList<Observer> observers;
private float temperature;
private float humiditty;
private float pressure;
public WeatherData1(){
observers=new ArrayList<Observer>();
}
//註冊
public void registerObserver(Observer o) {
observers.add(o);
}
//刪除
public void removeObserver(Observer o) {
int i=observers.indexOf(o);
if(i>=0){
observers.remove(i);
}
}
//通知觀察者資料變化
public void notifyObservers() {
for(int i=0;i<observers.size();i++){
Observer observer=observers.get(i);
observer.update(temperature, humiditty, pressure);
}
}
public void measurementsChanged(){
notifyObservers();
}
public void setMeasurements(float temperature,float humidity,float pressure){
this.temperature=temperature;
this.humiditty=humidity;
this.pressure=pressure;
measurementsChanged();
}
}
複製程式碼
5.CurrentConditionsDisplay
public class CurrentConditionsDisplay implements Observer,DisplayElement{
private float temperature;
private float humidity;
private float pressure;
private Subject weatherData;
public CurrentConditionsDisplay(Subject weatherData){
this.weatherData=weatherData;
weatherData.registerObserver(this);
}
/**
*
* update:更新佈告板內容
* @author 吉祥
* @param temperature
* @param humidity
* @param pressure
* @since JDK 1.6
*/
public void update(float temperature,float humidity,float pressure){
this.temperature=temperature;
this.humidity=humidity;
this.pressure=pressure;
display();
}
/**
*
* display:展示佈告板內容
* @author 吉祥
* @since JDK 1.6
*/
public void display(){
System.out.println("Current conditons:"+temperature
+"F degrees and "+humidity+"% humidity");
}
}
複製程式碼
6.ForecastDisplay
public class ForecastDisplay implements Observer,DisplayElement{
private float temperature;
private float humidity;
private float pressure;
private Subject weatherData;
public ForecastDisplay(Subject weatherData){
this.weatherData=weatherData;
weatherData.registerObserver(this);
}
/**
*
* update:更新佈告板內容
* @author 吉祥
* @param temperature
* @param humidity
* @param pressure
* @since JDK 1.6
*/
public void update(float temperature,float humidity,float pressure){
this.temperature=temperature;
this.humidity=humidity;
this.pressure=pressure;
display();
}
/**
*
* display:展示佈告板內容
* @author 吉祥
* @since JDK 1.6
*/
public void display(){
System.out.println("Forecast: More of the same");
}
}
複製程式碼
7.StatisticsDisplay public class StatisticsDisplay implements Observer,DisplayElement{ private float temperature; private float humidity; private float pressure; private Subject weatherData;
public StatisticsDisplay(SubjectweatherData){
this.weatherData=weatherData;
weatherData.registerObserver(this);
}
/**
*
* update:更新佈告板內容
* @author 吉祥
* @param temperature
* @param humidity
* @param pressure
* @since JDK 1.6
*/
public void update(float temperature,float humidity,float pressure){
this.temperature=temperature;
this.humidity=humidity;
this.pressure=pressure;
display();
}
/**
*
* display:展示佈告板內容
* @author 吉祥
* @since JDK 1.6
*/
public void display(){
System.out.println("Avg/Max/Min temperature= "+temperature
+"/"+temperature+"/"+temperature);
}
複製程式碼
}
ps:這裡在Observer中使用Subject原因在於方便以後的取消註冊。
最後我們建立一個測試類WeatherStation來進行測試
public class WeatherStation {
public static void main(String[] args){
WeatherData1 weatherData=new WeatherData1();
CurrentConditionsDisplay currentConditionsDisplay=new CurrentConditionsDisplay(weatherData);
StatisticsDisplay statisticsDisplay=new StatisticsDisplay(weatherData);
ForecastDisplay forecastDisplay=new ForecastDisplay(weatherData);
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 70, 29.2f);
weatherData.setMeasurements(78, 90, 29.2f);
}
}
複製程式碼
最終結果如下
到這裡我們已經講解完觀察者模式的一種實現方式。但是這我們也提出一個問題,用來發散。是否能夠在主題中提供向外的可以讓觀察者自己獲取自己想要資料,而並非將所有的資料都推送給觀察者?也就是在Push(推)的同時我們也可以pull(拉)。
4.Java內建的觀察者模式
剛才的問題,其實熟悉Java語言的開發者會發現,在Java中已經有相應的模式,如果熟悉的可以直接跳過本章。 在java.util包下有Observer和Observable類,這兩個類的結構跟我們遇到的Subject和Observer模型有些類似。甚至可是隨意使用push(推)或者pull(拉) 這裡我們使用線上的Java API網站線上Java API文件 首先查詢Observer的API
這個與我們所寫的Observer結構幾乎相同,只是在推送是把Observable類一起推送,這樣使用者既可以push也可以使用pull的方式。那麼Observable的結構呢
我們發現這裡Observable是類與我們之前Subject作為介面的方式稍微有區別;並且Observable類其他方法更全。那麼使用類的方式和使用介面的影響我們在後面會繼續講。並且這裡我們關注setChanged()方法告訴被觀察者的資料發生改變 那麼,如果要使用Java中自帶的觀察者模式來修改原有氣象站業務會如何。
首先,我們來分析更改後氣象站的模型:
4.1Java內建觀察者模式運作模式
相對於於之前Subject和Observer的模式,Java內建自帶的觀察者模式執行稍微有些差異。
-
將物件變成觀察者只需要實現Observer(java.util.Observer)介面,然後呼叫任何Observable的addObserver()方法即可。如果要刪除觀察者,呼叫deleteObserver()即可。
-
被觀察者若要推送通知,需要物件繼承Observable(java.util.Observable)類,並先呼叫setChanged(),首先標記狀態已經改變。然後呼叫notifyObservers()方法中的一個:notifyObservers()(通知觀察者pull資料)或notifyObserers(Object object)(通知觀察者push資料)
那麼作為觀察者如何處理被觀察者推送出的資料呢。 這裡邏輯如下:
- 觀察者(Observer)必須在update(Observable o,Object object).前一個引數用來讓觀察者知道是哪個被觀察者推送資料。後一個object為推送資料,允許為null。
4.2 setChanged()
在Observable類中setChanged()方法一開始我也有疑惑,為何在推送之前需要呼叫該方法。後來查閱資料和Java API發現它很好的一個用處。我們先來檢視java的原始碼
這裡必須標記為true才會推送訊息,那麼這個到底有何好處,我們拿氣象站模型來分析。 如果沒有setChanged方法,也是之前的Subject和Observer模型裡,一旦資料發生細微的變化,我們都會對所有的觀察者進行推送。如果我們需要在溫度變化1攝氏度以上才傳送推送,呼叫setChanged()方法更加有效。當然,這個功能使用場景很少,但是也不排除會用到。當然更改Object和Observer模型也是可以做到這個效果的!!!4.3 Java內建觀察者更改氣象站
那麼利用氣象站模型來實際操作一下,依照之前的模型我們程式碼應該如下 1.WeatherData2
public class WeatherData2 extends Observable{
private float temperature;
private float humidity;
private float pressure;
//構造器不需要為了記住觀察者建立資料模型
public WeatherData2(){
}
public void measurementsChanged(){
//在呼叫notifyObserver()需要指示狀態已經更改了
setChanged();
//這裡未使用notifyObserver(object),所以資料採用拉的邏輯
notifyObservers(this);
}
public void setMeasurements(float temperature,float humidity,float pressure){
this.temperature=temperature;
this.humidity=humidity;
this.pressure=pressure;
measurementsChanged();
}
//以下方法為pull操作提供
public float getTemperature() {
return temperature;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
}
複製程式碼
2.CurrentConditionsDisplay1
public class CurrentConditionsDisplay1 implements Observer,DisplayElement{
private Observable observable;
private float temperature;
private float humidity;
private float pressure;
//構造器需要傳入Observable引數,並登記成為觀察者
public CurrentConditionsDisplay1(Observable observable){
this.observable=observable;
observable.addObserver(this);
}
//update方法增加Observable和資料物件作為引數
public void update(Observable o, Object arg) {
if(arg instanceof WeatherData2){
WeatherData2 weatherData2=(WeatherData2) arg;
this.temperature=weatherData2.getTemperature();
this.humidity=weatherData2.getHumidity();
this.pressure=weatherData2.getPressure();
display();
}
}
/**
*
* display:展示佈告板內容
* @author 吉祥
* @since JDK 1.6
*/
public void display(){
System.out.println("Current conditons:"+temperature
+"F degrees and "+humidity+"% humidity");
}
}
複製程式碼
3.ForecastDisplay1
public class ForecastDisplay1 implements Observer,DisplayElement{
private float temperature;
private float humidity;
private float pressure;
private Observable observable;
public ForecastDisplay1(Observable observable){
this.observable=observable;
observable.addObserver(this);
}
public void update(Observable o,Object arg){
if(arg instanceof WeatherData2){
WeatherData2 weatherData2=(WeatherData2) arg;
this.temperature=weatherData2.getTemperature();
this.humidity=weatherData2.getHumidity();
this.pressure=weatherData2.getPressure();
display();
}
}
/**
*
* display:展示佈告板內容
* @author 吉祥
* @since JDK 1.6
*/
public void display(){
System.out.println("Forecast: More of the same");
}
}
複製程式碼
4.StatisticsDisplay1
public class StatisticsDisplay1 implements Observer,DisplayElement{
private float temperature;
private float humidity;
private float pressure;
private Observable observable;
public StatisticsDisplay1(Observable observable){
this.observable=observable;
observable.addObserver(this);
}
public void update(Observable o,Object arg){
if(arg instanceof WeatherData2){
WeatherData2 weatherData2=(WeatherData2) arg;
this.temperature=weatherData2.getTemperature();
this.humidity=weatherData2.getHumidity();
this.pressure=weatherData2.getPressure();
display();
}
}
/**
*
* display:展示佈告板內容
* @author 吉祥
* @since JDK 1.6
*/
public void display(){
System.out.println("Avg/Max/Min temperature= "+temperature
+"/"+temperature+"/"+temperature);
}
}
複製程式碼
最後進行測試: WeatherStation1
public class WeatherStation1 {
public static void main(String[] args){
WeatherData2 weatherData=new WeatherData2();
CurrentConditionsDisplay1 currentConditionsDisplay=new CurrentConditionsDisplay1(weatherData);
StatisticsDisplay1 statisticsDisplay=new StatisticsDisplay1(weatherData);
ForecastDisplay1 forecastDisplay=new ForecastDisplay1(weatherData);
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 70, 29.2f);
weatherData.setMeasurements(78, 90, 29.2f);
}
}
複製程式碼
結果最終如下:
我們對比之前Subject和Observer的觀察者模式會發現兩者輸出順序不一樣,這是為什麼?
其實java.util.Observable不依賴於觀察者被通知的順序的,並且實現了他的notifyObserver()方法,這會導致通知觀察者的順序不同於Subject和Observer模型在具體類實現notifyObserver()方法。其實兩者都沒有任何的程式碼誤差,只是實現的方式不同導致不同的結果。
但是java.util.Observable類卻違背了之前第一章中針對介面程式設計,而非針對實現程式設計。恐怖的是,它也沒有介面實現,這就導致它的使用具有很高的侷限性和低複用性。如果一個物件不僅僅是被觀察者,同時還是另一個超類的子類的時候,我們無法使用多繼承的方式來實現。我們如果自行擴充的話,你會發現setChanged()方法是protected方法,這就表示只有java.util.Observable自身和其子類才可以使用這個方法。這就違反了第二個設計原則---------"多用組合,少用繼承"。這也是我一般不會使用Java自帶的設計者模式的原因。
現在比較流行的觀察者模式,也就是RxJava,但是由於這個框架涉及不僅僅有觀察這模式,在之後整個設計模式整理玩不後,我會集中再講。
5.總結
到此,觀察者模式的講解已經全部講解完成。總結一下。
第四設計原則 為互動物件之間的鬆耦合涉及而努力
觀察者模式 在物件之間定義一對多的依賴,這樣一來,當一個物件改變狀態,依賴它的物件都會收到通知,並自動更新。
相應的資料和程式碼託管地址github.com/jixiang5200…