觀察者模式在實際開發中是一個使用率極高的一種設計模式,在設計模式中相比較來說也是必須要掌握的模式之一,觀察者模式(有時又被稱爲發佈(publish)- 訂閱者(Subscribe)模式、模型 - 視圖(View)模式等)是軟件設計的一種模式,在此種模式中,一個目標物件管理所有相依賴於它的觀察者物件,並且在它本身的狀態發生改變時主動發出通知,此種模式通常來實現事件處理系統
一、觀察者模式基本介紹
觀察者模式(Observer)完美的將觀察者和被觀察者的對象分離開,舉個例子,用戶界面可以作爲一個觀察者,業務數據則是被觀察者,用戶界面觀察業務數據的變化,發現數據變化後,就顯示在界面上,面向對象的設計的一個原則是:系統中的每個類將重點放在某一個功能上,而不是其他方面,一個對象只做一件事情,並且將它做好,觀察者模式在模塊之間劃定了清晰的界限,提高了應用程序的可維護性和重用性
二、觀察者模式的定義
定義對象間的一種一對多的依賴關係,使得每當一個對象改變狀態,則所有依賴於它的對象都會得到通知並自動更新
三、觀察者模式的使用場景
當你的項目中有多對一的依賴關係,可以考慮使用,因爲觀察者模式就是針對對象之間多對一依賴的一種設計方案,被依賴的對象爲 Subject,依賴的對象爲 Observer,Subject 通知 Observer 變化
四、觀察者模式的模型圖
1. Subject: 抽象主題,也就是被觀察者(Observerable)的角色,抽象主題角色把所有觀察者對象的引用保存在一個集合裏,每個主題都可以有任意數量的觀察者,抽象主題提供一個接口,可以增加和刪除觀察者對象
2. ConcreteSubject: 具體主題,該角色將有關狀態存入具體觀察者對象,在具體主題的內部狀態發生改變時,給所有註冊過的觀察者發出通知,具體主題角色又叫做具體被觀察者(ConcreteObserverable)角色
3. Observer: 抽象觀察者,該角色是觀察者的抽象類,它定義了一個更新接口,使得在得到主題的更改通知時更新自己
4. ConcreteObserver: 具體觀察者,該角色實現抽象觀察者角色所定義的更新接口,以便在主題的狀態發生變化時更新自身的狀態
五、觀察者模式實例演示
前幾天閱讀《Head First》一書學習觀察者模式時覺得裏面舉的例子就挺好,這裏我們暫且也借用裏面的例子來演繹,我們從從一個項目的需求實際出發,從普通的實現方法和用觀察者模式實現的不同角度來對比在一些特定的場景模式下,使用觀察者模式解決相應問題的方便的可擴展性以及便捷性
需求:加入我們的團隊剛剛接到一份合約,負責建立一個氣象站應用,這個應用有兩個公告板,可以分別顯示目前的天氣狀況,當 WeatherObject 對象獲得最新的測量數據時,公告板必須隨時更新,而且這是一個可以擴展的氣象站,氣象站希望可以公佈一組 API,讓其他開發人員可以寫出自己的公告板,可以插入此應用中,模型圖如下:
WeatherData 知道如何和氣象站取得聯繫,來獲得天氣的相關數據,當天氣數據發生變化時,WeatherData 會更新相應的公告牌實時展示天氣數據
1)先來看普通的實現方法
我們先來創建 WeatherData 類,實例代碼如下
/**
* 從氣象站獲取的天氣數據的 WeatherData 類
* 具有getter方法可以獲取到溫度、溼度、氣壓的測量值
* Created by qiudengjiao on 2017/6/5.
*/
public class WeatherData {
private float mTemperatrue;
private float mHumidity;
private float mPressure;
private CurrentConditionsDisplay mCurrentConditionsDisplay;
public WeatherData(CurrentConditionsDisplay currentConditionsDisplay) {
this.mCurrentConditionsDisplay = currentConditionsDisplay;
}
public float getTemperatrue() {
return mTemperatrue;
}
public float getHumidity() {
return mHumidity;
}
public float getPressure() {
return mPressure;
}
/**
* 模擬氣象站更新數據
*
* @param mTemperatrue
* @param mHumidity
* @param mPressure
*/
public void setData(float mTemperatrue, float mHumidity, float mPressure) {
this.mTemperatrue = mTemperatrue;
this.mHumidity = mHumidity;
this.mPressure = mPressure;
dataChange();
}
/**
* 調用WeatherData的getXXX方法,以取得最新的測量值
* 通知公告板數據更新
*/
public void dataChange() {
mCurrentConditionsDisplay.updata(getTemperatrue(), getHumidity(), getPressure());
}
}
WeatherData 類主要是獲取氣象站傳過來的數據,並通過 dataChange() 方法通知公告板進行實時的更新,實現的比較簡單,相信大家一看就能明白
接下來看當前天氣公告板 CurrentConditionsDisplay 類:
/**
* 當前天氣展示公告板
* Created by qiudengjiao on 2017/6/5.
*/
public class CurrentConditionsDisplay {
private float mTemperatrue;
private float mHumidity;
private float mPressure;
/**
* 更新數據
*
* @param mTemperatrue
* @param mHumidity
* @param mPressure
*/
public void updata(float mTemperatrue, float mHumidity, float mPressure) {
this.mTemperatrue = mTemperatrue;
this.mHumidity = mHumidity;
this.mPressure = mPressure;
display();
}
/**
* 模擬公告板顯示
*/
public void display() {
System.out.println("今天的溫度:::" + mTemperatrue);
System.out.println("今天的溼度:::" + mHumidity);
System.out.println("今天的氣壓:::" + mPressure);
}
}
這裏也非常簡單,就是把從 WeatherData 類從氣象站中獲取的傳遞過來的數據進行展示,這裏我們用 updata 來傳遞數據,用 display 模擬顯示裝置來展示數據
現在我們來測試我們寫的層序是否可行:
/**
* 測試類
*/
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
}
private void initData() {
CurrentConditionsDisplay currentConditionsDisply = new CurrentConditionsDisplay();
WeatherData weatherData = new WeatherData(currentConditionsDisply);
//模擬設置相應的參數
weatherData.setData(30, 50, 130);
}
}
打印效果如下:
這樣我們就實現了相應的需求,但是現在我們來思考一下,我們的實現有什麼不對?
這裏我們的實現是針對具體的編程實現,試想一下,我們現在又有新的公告板,或者想刪除剛纔創建的公告板,這樣我們就需要不停的去修改我們的程序,去增加或者刪除公告板,不停地修改我們的程序顯然不利於擴展,接下來我們就來看看着我們的項目中使用觀察者模式是如何解決這些問題的
2)用觀察者模式來實現
2.1 首先設計氣象站:
先來看一下這個設計圖,這張設計圖其實是在我們前面模型圖的擴展:
這裏,Subject 抽象接口我們並不陌生,是我們的主題接口,現在 WeahterData 實現 Subject 接口,然後是觀察者的抽象接口 Observer,所有氣象組件(例如我們的公告板)都需要實現此觀察者接口,這樣主題在需要通知觀察者時,有了一個共同的接口,然後我們也爲公告板告板建立了一個共同的接口,公告板只需要實現 display() 方法
2.2 實現氣象站
現在我們開始來實現這個系統了,Java 爲觀察者模式提供了內置支持,我們先暫時不用它,先自己動手,雖然有些時候可以利用 Java 內置支持,但是有些時候我們自己建立一個,這樣會更具有彈性,那我們就從建立接口開始吧
首先建立抽象主題 Subject 接口:
/**
* 抽象主題接口
* Created by qiudengjiao on 2017/6/6.
*/
public interface Subject {
/**
* 註冊觀察者,需要一個觀察者作爲變量
*
* @param o
*/
public void registerObserver(Observer o);
/**
* 刪除觀察者,需要一個觀察者作爲變量
*
* @param o
*/
public void removeObserver(Observer o);
/**
* 當主題發生改變時,這個方法會被調用,以通知所有觀察者
*/
public void notifyObservers();
}
接下來建立抽象觀察者 Observer 接口:
/**
* 抽象觀察者接口
* Created by qiudengjiao on 2017/6/6.
*/
public interface Observer {
/**
* 所有的觀察者都必須實現update()方法,以實現觀察者接口
* 當氣象觀察值發生改變時,主題會把以下狀態值當做方法的參數,傳送給觀察者
*
* @param temperatrue
* @param humidity
* @param pressure
*/
public void update(float temperatrue, float humidity, float pressure);
}
然後我們再來建立公告板公共顯示 DisplayElement 接口:
/**
* DisplayElement接口只包含了一個方法display
* Created by qiudengjiao on 2017/6/6.
*/
public interface DisplayElement {
/**
* 當公告板需要顯示時調用此方法
*/
public void display();
}
由於我們在代碼中已經做了詳細的註解,就不再一一具體講解
2.3 在 WeatherData 中實現 Subject 主題接口
/**
* WeathDate類
*/
public class WeatherDate implements Subject {
// 用ArraList來記錄觀察者
private ArrayList observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherDate() {
observers = new ArrayList();
}
/**
* 註冊觀察者
*/
@Override
public void regiserObserver(Observer o) {
observers.add(o);
}
/**
* 刪除觀察者
*/
@Override
public void removeObserver(Observer o) {
int i = observers.indexOf(o);
if (i >= 0) {
observers.remove(i);
}
}
/**
* 把狀態通知每一個觀察者,因爲觀察者都通知實現了update 所有我們知道如何通知它
*/
@Override
public void notifyObserver() {
for (int i = 0; i < observers.size(); i++) {
Observer observer = (Observer) observers.get(i);
observer.update(temperature, humidity, pressure);
}
}
/**
* 當氣象站得到更新觀測值時,我們通知觀察者
*/
public void dateChange() {
notifyObserver();
}
/**
* 模擬設置數據
*
* @param temperature
* @param humidity
* @param pressure
*/
public void setDate(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
dateChange();
}
}
2.4 現在我們來建立公告板
/**
* 此公告板實現了Observer接口,所以可以從WeatherDate對象中獲得改變
* 它也實現了DisplayElement接口,因爲我們規定,所用的公告板都行必須實現此接口
*
*/
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private Subject weatherDate;
private float temperature;
private float humidity;
private float pressure;
/**
* 構造器需要WeatherDate對象(也就是主題),作爲註冊之用
*
* @param weatherDate
*/
public CurrentConditionsDisplay(WeatherDate weatherDate) {
this.weatherDate = weatherDate;
weatherDate.regiserObserver(this);
}
/**
* 當update調用時,我們把相應的參數保存起來 然後調用display()
*/
@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
display();
}
/**
* display方法把今天的天氣情況展示出來
*/
@Override
public void display() {
System.out.println("今天的溫度是:::" + temperature);
System.out.println("今天的溼度是:::" + humidity);
System.out.println("今天的氣壓是:::" + pressure);
}
}
2.5 我們來測試一下,看看情況
/**
* 測試類
*/
public class WeatherStation {
public static void main(String[] args) {
// 首先,創建WeatherDate對象
WeatherDate weatherDate = new WeatherDate();
// 建立公告板
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherDate);
// 調用setDate,模擬設置氣象數據
weatherDate.setDate(30, 40, 150);
}
}
2.6 測試臺打印情況如下:
這樣我們就成功實現了天氣情況的展示,在使用觀察者模式之後,和原來的實現方式相比就有了更好的擴展性,很好的解決了我們的問題,接下來我們再來創建一個新的公告板,看看擴展效果
2.7 建立一個新的公告板
/**
* 天氣預報公告板
*/
public class ForecastDisplay implements Observer, DisplayElement {
private Subject weatherDate;
private float temperature;
private float humidity;
private float pressure;
/**
* 構造器需要WeatherDate對象(也就是主題),作爲註冊之用
*
* @param weatherDate
*/
public ForecastDisplay(WeatherDate weatherDate) {
this.weatherDate = weatherDate;
weatherDate.regiserObserver(this);
}
@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
display();
}
@Override
public void display() {
System.out.println("明天的溫度是:::" + temperature);
System.out.println("明天的溼度是:::" + humidity);
System.out.println("明天的氣壓是:::" + pressure);
}
}
2.8 現在我們來修改一下測試類,代碼如下:
/**
* 測試類
*/
public class WeatherStation {
public static void main(String[] args) {
// 首先,創建WeatherDate對象
WeatherDate weatherDate = new WeatherDate();
// 建立公告板
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherDate);
// 建立預測公告板
ForecastDisplay forecastDisplay = new ForecastDisplay(weatherDate);
// 調用setDate,模擬設置氣象數據
weatherDate.setDate(30, 40, 150);
}
}
這裏我們添加了新的公告板,下面我們來看看打印的結果:
這樣新加入的公告板就實現了,解決了修改代碼的問題,使得程序更加的易於擴展,也更加的健壯,大家可以自行模擬刪除公告板,也是非常的簡單,這裏我們就不再演示,到目前爲止,我們已經從無到有的完成了觀察者模式,但是 Java API 有內置的觀察者模式,java.util(package)內包含最基本的 Observer 接口與 Observable 類,這和我們的 Subject 接口與 Observer 接口很相似, Observer 接口與 Observable 類使用上更加方便,因爲許多功能都已經實現好了,接下來我們就來看使用 Java 內置的觀察者模式如何來實現我們剛纔的需求
六、使用 Java 內置的觀察者模式
首先,把 WeatherDate 改成使用 java.util.Observable
import java.util.Observable;
/**
* 從氣象站獲取的天氣數據的 WeatherData 類
* 現在繼承內置的 Observable
* 我們不需要追蹤觀察者了,也不需要管理註冊與刪除(超類代替即可)
* 所以我們把註冊和刪除的代碼刪掉
* Created by qiudengjiao on 2017/6/5.
*/
public class WeatherData extends Observable {
private float mTemperatrue;
private float mHumidity;
private float mPressure;
/**
* 構造函數也不用爲了記住觀察者們而創建數據集合了
*/
public WeatherData() {
}
/**
* 當氣象站得到跟新觀測值時,通知觀察者
*/
public void dataChange() {
//在調用notifyObservers()之前,必須要先調用setChanged()來指示狀態已經改變
setChanged();
notifyObservers();
}
/**
* 模擬氣象站設置更新數據
*
* @param temperatrue
* @param humidity
* @param pressure
*/
public void setData(float temperatrue, float humidity, float pressure) {
this.mTemperatrue = temperatrue;
this.mHumidity = humidity;
this.mPressure = pressure;
dataChange();
}
public float getTemperatrue() {
return mTemperatrue;
}
public float getHumidity() {
return mHumidity;
}
public float getPressure() {
return mPressure;
}
}
接下來我們重做 CurrentConditionsDisplay 公告板:
import java.util.Observable;
import java.util.Observer;
/**
* 當前天氣展示公告板
* 實現Java內置的Observer接口
* Created by qiudengjiao on 2017/6/5.
*/
public class CurrentConditionsDisplay implements Observer {
private Observable observable;
private float mTemperatrue;
private float mHumidity;
private float mPressure;
/**
* 現在構造函數把Observable當參數
* 並將CurrentConditionsDisplay登機爲觀察者
*
* @param observable
*/
public CurrentConditionsDisplay(Observable observable) {
this.observable = observable;
observable.addObserver(this);
}
/**
* 改變update()方法,增加Observable和數據對象作爲參數
*
* @param o
* @param arg
*/
@Override
public void update(Observable o, Object arg) {
if (o instanceof WeatherData) {
WeatherData weatherData = (WeatherData) o;
this.mTemperatrue = weatherData.getTemperatrue();
this.mHumidity = weatherData.getHumidity();
this.mPressure = weatherData.getPressure();
display();
}
}
/**
* 模擬公告板顯示
*/
public void display() {
System.out.println("今天的溫度:::" + mTemperatrue);
System.out.println("今天的溼度:::" + mHumidity);
System.out.println("今天的氣壓:::" + mPressure);
}
}
測試類:
/**
* 測試類
*/
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
}
private void initData() {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentConditionsDisply = new CurrentConditionsDisplay(weatherData);
//模擬設置相應的參數
weatherData.setData(30, 50, 130);
}
}
我們來看看控制檯打印結果如下:
這樣我們就在使用 Java 內置類的情況下也實現了業務需求,接下來我們來分析下使用 Java 內置方法有一點的不好處,這裏大家注意到Observable 是一個類,你必須設計一個類去繼承它,如果某類想同時具有 Observable 和另一個超類時,就會陷入兩難,畢竟 Java 不支持多繼承,這限制了 Observable 的複用潛力(而增加複用潛力正是我們設計模式的最原始動機),所以大家在使用觀察者模式時也要相應的注意,好了,觀察者模式我們到這裏就討論完畢了