Observer Pattern(觀察者模式)定義: 在對象之間定義一對多的依賴,這樣一來,當一個對象改變狀態,依賴它的對象都會收到通知,並自動更新。 乾說定義肯定沒有舉例理解的透徹。想到Observer Pattern(觀察者模式)就來舉個生活中的例子來幫助我們更好消化和理解其具體含義。 舉例: ...
Observer Pattern(觀察者模式)定義:
在對象之間定義一對多的依賴,這樣一來,當一個對象改變狀態,依賴它的對象都會收到通知,並自動更新。
乾說定義肯定沒有舉例理解的透徹。想到Observer Pattern(觀察者模式)就來舉個生活中的例子來幫助我們更好消化和理解其具體含義。
舉例:
訂閱雜誌或者報紙,這裡面有兩個主角,一個是報紙雜誌的供應商(報社),一個是報紙雜誌的訂閱者。這裡就是被觀察者也叫主題(供應商)和觀察者(訂閱者)。主題(Subject)應該有觀察者名單,當主題有新的報紙售出時將按主體持有的觀察者名單一個一個發送新報紙(發送沒有先後順序,一切按存儲順序發送)。
同時主題還應該有三個方法:
一、將觀察者寫入名單中(registerObserver())
二、將觀察者從名單中刪除(removeObserver())
三、當有新消息發送及時通知名單中所有觀察者(notifyObservers())
1 public interface Subject {//主題介面,所有報社都要實現該介面 2 3 /*沒有存儲訂閱者的列表,是因為我們不想在介面中寫死存儲方式, 4 讓編程人員自己在實現介面的時候寫入想要的存儲方式(如:鏈表,數組,棧,隊列等) 5 這樣更合理。*/ 6 7 public void registerObserver(Observer o);//將訂閱者登記在列表中 8 public void removeObserver(Observer o);//將訂閱者從列表中移除 9 public void notifyObservers(Object arg);//有參通知方法,有新的消息即使通知列表中所有訂閱者 10 public void notifyObservers();//無參通知方法 11 }Subject
觀察者所具有的東西就會少一些:
首先,內部需要有存儲主題的對象,這樣知道觀察者所訂閱的報社是哪一家,具有主題對象還有一個重要的原因,把登記、刪除、通知觀察者的功能全部委托給主題去做。
其次,還需要有更新自己消息的方法(update())新的消息發送過來,觀察者也要及時更新自己內部消息,將舊的消息替換成新的消息。
1 public interface Observer{//所有訂閱者要實現的介面 2 /*在介面中,沒有主題對象,也是因為不想將主題對象寫死在介面中, 3 在具體類中寫入更好*/ 4 5 public void update(Subject sub, Object args);//接收到新消息,及時更新 6 }Observer
現在,我們來以具體的生活例子來介紹如何實現觀察者模式(Observer Pattern):
有一家氣象站,氣象站本身已經具有WeatherData對象(相當於報社功能,可以獲得目前的溫度、濕度、氣壓三種數據)。
我們需要編寫一個應用,該應用有很多種顯示模式(從溫度、濕度、氣壓中任選一到三個組合就是一種模式)。
當WeatherData對象獲得最新的測量數據時,我們的應用可以及時更新顯示模式中的數據。
根據要求寫程式:
主題介面:
1 public interface Subject {//主題介面,所有報社都要實現該介面 2 3 /*沒有存儲訂閱者的列表,是因為我們不想在介面中寫死存儲方式, 4 讓編程人員自己在實現介面的時候寫入想要的存儲方式(如:鏈表,數組,棧,隊列等) 5 這樣更合理。*/ 6 7 public void registerObserver(Observer o);//將訂閱者登記在列表中 8 public void removeObserver(Observer o);//將訂閱者從列表中移除 9 public void notifyObservers(Object arg);//有參通知方法,有新的消息即使通知列表中所有訂閱者 10 public void notifyObservers();//無參通知方法 11 }Subject
主題介面實體類:
1 import java.util.ArrayList; 2 3 public class WeatherData implements Subject{//氣象站的實現類 4 private ArrayList observers;//觀察者列表 5 private float temperature;//數據之一:溫度 6 private float humidity;//數據之二:濕度 7 private float pressure;//數據之三:氣壓 8 private boolean status;//數據是否更新的標誌 9 10 public WeatherData(){//初始化時,為觀察者列表賦值 11 observers = new ArrayList(); 12 } 13 14 public float getTemperature(){//獲取溫度的方法 15 return this.temperature; 16 } 17 18 public float getHumidity(){//獲取濕度的方法 19 return this.humidity; 20 } 21 22 public float getPressure(){//獲取氣壓的方法 23 return this.pressure; 24 } 25 26 public void registerObserver(Observer o){//將觀察者記錄在列表中 27 observers.add(o); 28 } 29 30 public void removeObserver(Observer o){//將觀察者從列表中刪除 31 int i = observers.indexOf(o); 32 observers.remove(i); 33 } 34 35 public void notifyObservers(Object args){//有參通知觀察者方法 36 if(status){//判斷數據是否有更新 37 for(int i = 0; i < observers.size(); i++){ 38 Observer observer = (Observer) observers.get(i); 39 observer.update(this, args); 40 } 41 status = false;//消息發送成功後,將更新標誌位重置 42 } 43 } 44 45 public void notifyObservers(){//無參通知觀察者方法 46 notifyObservers(null); 47 } 48 49 public void setChange(){//數據是否更新的標誌 50 status = true; 51 } 52 53 public void setMeasurements(float temperature, float humidity, float pressure){//數據更新方法 54 this.temperature = temperature; 55 this.humidity = humidity; 56 this.pressure = pressure; 57 measurementsChanged(); 58 } 59 60 public void measurementsChanged(){//數據改變後調用該方法 61 setChange(); 62 notifyObservers(); 63 } 64 65 66 }WeatherData
觀察者介面:
1 public interface Observer{//所有訂閱者要實現的介面 2 /*在介面中,沒有主題對象,也是因為不想將主題對象寫死在介面中, 3 在具體類中寫入更好*/ 4 5 public void update(Subject sub, Object args);//接收到新消息,及時更新 6 }Observer
顯示更新數據的介面:
1 public interface DisplayElement{//顯示更新數據的介面 2 public void display(); 3 }DisplayElement
觀察者介面實體類:
1 public class CurrentConditionsDisplay implements Observer, DisplayElement{//顯示當前溫度、濕度的類 2 private WeatherData weatherData;//定義訂閱的主題對象 3 private float temperature;//溫度數據 4 private float humidity;//濕度數據 5 6 public CurrentConditionsDisplay(WeatherData weatherData){ 7 this.weatherData = weatherData; 8 weatherData.registerObserver(this);//將該觀察者對象登記在主題的觀察者列表中 9 } 10 11 public void update(Subject sub, Object args){//數據更新方法 12 if(sub instanceof WeatherData){ 13 WeatherData weatherData = (WeatherData) sub; 14 this.temperature = weatherData.getTemperature(); 15 this.humidity = weatherData.getHumidity(); 16 display(); 17 } 18 } 19 20 public void display(){//顯示數據的方法 21 System.out.println("Current conditions:" + temperature + "F degrees and " + humidity + "%humidity"); 22 } 23 }CurrentConditionsDisplay
測試類:
1 public class WeatherStation{ 2 public static void main(String[] agrs){ 3 WeatherData weatherData = new WeatherData(); 4 5 CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData); 6 7 weatherData.setMeasurements(80, 65, 30.4f);//更新數據 8 weatherData.setMeasurements(82, 70, 29.2f);//更新數據 9 weatherData.setMeasurements(79, 90, 29.2f);//更新數據 10 } 11 }WeatherStation
編譯運行結果:
上面代碼已經很完善了,而且不知道你有沒有發現,其實每次WeatherData更新數據都是把所有數據都更新,但是我們的CurrentConditionsDispaly只獲取溫度和濕度兩個數據,並且從來不獲取多餘的氣壓數據。這就是數據推送(Push)和數據抽取(Pull)的區別。
Push:不管你有沒有訂閱該數據,主題都會將該數據發送給訂閱者,再由訂閱者決定數據的取捨,沒用的數據就不會記錄在自己的數據中。
Pull:訂閱者想要什麼數據由訂閱者說了算,主題只需提供獲取數據的方法(get...())就好,而上面我們的氣象站就是使用了數據抽取方式。
思想提煉:
1.多用組合,少用繼承
2.為交互對象之間的松耦合設計而努力