1. 程式人生 > >Head First設計模式 第二章:觀察者模式

Head First設計模式 第二章:觀察者模式

觀察者模式是JDK中使用最多的模式之一。

氣象監測應用:

任務:

WeatherData物件負責追蹤目前的天氣狀況(溫度、溼度、氣壓)。建立一個應用,有三種佈告板,分別顯示目前的狀況、氣象統計及簡單的預報。當WeatherObject 物件獲得最新的測量資料時, 三種佈告板必須實時更新。這個應用應當可以擴充套件,寫出一組API,可以讓其他開發人員可以寫出自己的氣象佈告板,井插入此應用中。

此係統中的三個部分是氣象站(獲取實際氣象資料的物理裝置)、WeatherData物件(追蹤來自氣象站的資料,並更新佈告板)和佈告板(顯示目前天氣狀況給使用者看)。

如圖:

WeatherData物件知道如何跟物理氣象站聯絡,以取得更新的資料。WeatherData物件會隨即更新三個佈告板的顯示: 目前狀況(溫度、溼度、氣壓)、氣象統計和天氣預報。

如果我們選擇接受這個專案, 我們的工作就是建立一個應用, 利用WeatherData物件取得資料, 井更新三個佈告板:目前狀況、氣象統計和天氣預報。

WeatherData類:

我們目前知道:

WeatherData類具有getter方法,可以取得三個測量值:溫度、溼度與氣壓;

當新的測量資料備妥時,measurementsChanged()方法就會被呼叫(我們不在乎此方法是如何袚呼叫的,我們只在乎它被呼叫了);

我們需要實現三個使用天氣資料的佈告板: “目前狀況“佈告、“ 氣象統計” 佈告、“天氣預報”佈告。一且WeatherData有新的測量, 這些佈告必須馬上更新;

此係統必須可擴充套件,讓其他開發入員建立定製的佈告板,使用者可以隨心所欲地新增或刪除任何佈告板。目前初始的佈告板有三類: “ 目前狀況“佈告、“氣象統計“佈告、“天氣預報“佈告。

一個錯誤的範例:

錯誤的地方:

觀察者模式:主題物件與觀察者物件的關係

鴨子物件變為觀察者物件:

老鼠物件想從觀察者除名:

觀察者模式:

觀察者模式定義了物件之間的一對多依賴,這樣一來,當一個物件改變狀態時,它的所有依賴者都會收到通知並自動更新。觀察者模式定義了一系列物件之間的一對多關係。觀察者依賴於此主題,只要主題狀態一有變化,觀察者就會被通知。根據通知的風格,觀察者可能因此新值而更新。

如:

實現觀察者模式的方法不只一種,但是以包含Subject與Observer介面的類設計的做法最常見。

定義觀察者模式:類圖

主題是具有狀態的物件, 並且可以控制這些狀態。因為主題是真正擁有資料的人,觀察者是主題的依賴者,在資料變化時更新,這樣比起讓許多物件控制同一份資料,可以得到更乾淨的OO設計。

鬆耦合:

觀察者模式提供了一種物件設計, 讓主題和觀察者之間鬆耦合。

關於觀察者的一切,主題只知道觀察者實現了某個介面(也就是Observer介面)。主題不需要知道觀察者的具體類是誰、做了些什麼或其他任何細節。

任何時候我們都可以增加新的觀察者。因為主題唯一依賴的東西是一個實現Observer介面的物件列表,所以我們可以隨時增加觀察者。同樣我們也可以刪除觀察者。

有新型別的觀察者出現時,主題的程式碼不需要修改。它只會傳送通知給所有實現了觀察者介面的物件。

改變主題或觀察者其中一方,並不會影響另一方。因為兩者是鬆耦合的,所以只要他們之間的介面仍被遵守,我們就可以自由地改變他們。

設計原則:

為了互動物件之間的鬆耦合設計而努力。鬆耦合的設計之所以能讓我們建立有彈性的OO系統,能夠應對變化,是因為物件之間的互相依賴降到了最低。

回到氣象站專案:

使用觀察者模式,WeatherData類是“主題”,各種佈告板是“觀察者”。佈告板要稱為主題的觀察者必須先向WeatherData物件註冊。一且WeatherData知道有某個佈告板的存在,就會適時地呼叫佈告板的某個方法來告訴佈告板改變的觀測值是多少。

儘管佈告板的類都不一樣,但是它們都應該實現相同的介面,好讓WeatherData物件能夠知道如何把觀測值送給它們。因此每個佈告板都應該有一個update()的方法,供WeatherData物件呼叫。這個update()方法應該在所有佈告板都實現的共同接口裡定義。

設計圖:

自建Subject介面和Observer介面進行氣象站的程式碼實現:

先建立Subjcet、Observer、DisplayElement介面。

在WeatherData類中實現主題介面。

建立其中一個佈告板類。

程式碼如下:

//subject介面
public interface Subject {
	public void registerObserver(Observer o);

	public void removeObserver(Observer o);

	public void notifyObservers();
}
//Observer介面
public interface Observer {
	public void update(float temp, float humidity, float pressure);
}
//DisplayElement介面
public interface DisplayElement {
	public void display();
}
//WeatherData類
import java.util.ArrayList;

public class WeatherData implements Subject {
	private ArrayList observers;
	private float temperature;
	private float humidity;
	private float pressure;
    
    //類的構造方法
	public WeatherData() {
		observers = new ArrayList();
	}

	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 = (Observer) observers.get(i);
			observer.update(temperature, humidity, pressure);
		}
	}

	public void measurementsChanged() {
		notifyObservers();
	}

	public void setMeasurements(float temperature, float humidity, float pressure) {
		this.temperature = temperature;
		this.humidity = humidity;
		this.pressure = pressure;
		measurementsChanged();
	}
}
//CurrentConditionsDisplay類
public class CurrentConditionsDisplay implements Observer, DisplayElement {
	private float temperature;
	private float humidity;
	private Subject weatherData;

    //類的構造方法
	public CurrentConditionsDisplay(Subject weatherData) {
		this.weatherData = weatherData;
		weatherData.registerObserver(this);
        //this關鍵字把當前物件傳遞給其他方法
	}

	public void update(float temperature, float humidity, float pressure) {
		this.temperature = temperature;
		this.humidity = humidity;
		display();
	}

	public void display() {
		System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + " % humidity ");
	}
}
//WeatherStation測試類
public class Main {
	public static void main(String[] args) {
        //建立一個WeatherData類的物件,構造方法建立了一個ArrayList用來存放observer
		WeatherData weatherData = new WeatherData();
        //建立一個CurrentConditionsDisplay類的物件
        //this是類就是CurrentConditionsDisplay的成員物件,構造方法初始化時將賦給this的Subject屬性一個值,值就是傳入的weatherData物件,然後註冊this到ArrayList陣列中,此時陣列中就有了一個obersver物件
		CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
		//StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
		//ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
        
        //傳入更新的資料,setMeasurements會呼叫measurementsChanged()方法,measurementsChanged()方法又會呼叫notifyObservers()方法,該方法定義了一個迴圈,每次從ArryList中取出一個observer物件,然後對其呼叫update()方法
        //又observer物件均是從ArrayList陣列中取出,這些物件都是在CurrentConditionsDisplay類構造方法中將CurrentConditionsDisplay類的成員變數註冊到ArrayList陣列中的,它們都是CurrentConditionsDisplay類的成員變數,故呼叫update()方法時會呼叫到CurrentConditionsDisplay類中覆寫後的update()方法,update()方法又會呼叫覆寫後的display()方法,這樣就會打印出類似如下一行結果:Current conditions: 80.0F degrees and 65.0 % humidity 
		weatherData.setMeasurements(80, 65, 30.4f);
		weatherData.setMeasurements(82, 70, 29.2f);
		weatherData.setMeasurements(78, 90, 29.2f);
	}
}

執行截圖如下:

上面主題向觀察者推送訊息使用的是“推”的方式。

事實上,對於主題向觀察者推送訊息的方式有兩種:“推”(主題主動)和“拉”(觀察者主動)。兩種做法都有各自的優點。Java內建的Observer 模式兩種做法都支援。

使用Java內建的觀察者模式的程式碼實現:

java. util包(package) 內包含最基本的Observer介面與Observable類。

修改後的設計圖:

現在我們的WeatherData (也就是我們的主題)現在擴充套件自Observable類,並繼承一些增加刪除、通知觀察者的方法(以及具他的方法)。

物件變成觀察者:

實現觀察者介面(java.uitl.Observer) , 然後呼叫任何Observable 物件的addObserver()方法。不想冉當觀察者時,呼叫deleteObserver()方法就可以了。

觀察者送出通知:

利用擴充套件java.util.Observable介面產生”可觀察者”類。先呼叫setChanged()方法,標記狀態已經改變的事實。然後呼叫兩種notifyObservers()方法中的一個:notifyObservers()或notifyObservers(Object arg)。

觀察者接收通知:

update (Observable o,object arg)。第一個引數是主題,第二個引數則是要接收通知的觀察者物件。

程式碼實現:

首先, 把WeatherData改成使用java.util.Observable。

重做CurrentConditionsDispiay。

程式碼如下:

//DisplayElement介面
public interface DisplayElement {
	public void display();
}
//WeatherData類(繼承Observable介面)
import java.util.Observable;
import java.util.Observer;

public class WeatherData extends Observable {
	private float temperature;
	private float humidity;
	private float pressure;

    //現在用預設構造方法,不再構造一個儲存觀察者物件的ArrayList
	public WeatherData() {
	}

	public void measurementsChanged() {
        //先設定狀態改變,再通知觀察者
		setChanged();
		notifyObservers();
	}

	public void setMeasurements(float temperature, float humidity, float pressure) {
        //傳入改變的引數
		this.temperature = temperature;
		this.humidity = humidity;
		this.pressure = pressure;
		measurementsChanged();
	}

	//下面3個不是新方法,寫出來是為了提醒一下有這些方法
	public float getTemperature() {
		return temperature;
	}

	public float getHumidity() {
		return humidity;
	}

	public float getPressure() {
		return pressure;
	}
}
//CurrentConditionsDisplay類
import java.util.Observable;
import java.util.Observer;

public class CurrentConditionsDisplay implements Observer, DisplayElement {
	Observable observable;
	private float temperature;
	private float humidity;
    
    //類的構造方法,設定成員變數屬性Observable的值為observable,即傳入的物件,然後註冊新增到觀察者佇列中
	public CurrentConditionsDisplay(Observable observable) {
		this.observable = observable;
		observable.addObserver(this);
	}

	public void update(Observable obs, Object arg) {
        //instanceof判斷其左邊物件是否為其右邊類的例項
		if (obs instanceof WeatherData) {
            //強制型別轉換
			WeatherData weatherData = (WeatherData) obs;
			this.temperature = weatherData.getTemperature();
			this.humidity = weatherData.getHumidity();
			display();
		}
	}

	public void display() {
		System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
	}
}
//WeatherStation測試類
public class Main {
	public static void main(String[] args) {
        //建立一個WeatherData類的物件,構造方法為預設
		WeatherData weatherData = new WeatherData();
        //建立一個CurrentConditionsDisplay類的物件
        //this是類就是CurrentConditionsDisplay的成員物件,構造方法初始化時將賦給this的Subject屬性一個值,值就是傳入的weatherData物件,然後註冊this到observer觀察者佇列中
		CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
		//StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
		//ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
        
        //傳入更新的資料,setMeasurements會呼叫measurementsChanged()方法,measurementsChanged()方法會呼叫setChanged()表示狀態已改變,然後呼叫notifyObservers()方法來通知觀察者
		weatherData.setMeasurements(80, 65, 30.4f);
		weatherData.setMeasurements(82, 70, 29.2f);
		weatherData.setMeasurements(78, 90, 29.2f);
	}
}

執行截圖如下: