Observer Pattern 觀察者模式 [Design Pattern in Java]

Posted by Kubeguts on 2020-07-26

觀察者模式可以讓物件了解資料變化的情況。
物件甚至可以在執行期間決定是否要繼續被通知,又或者是可以主動去詢問資料的狀態。
在此模式中也會了解一對多,以及物件鬆綁的意義是如何。

以氣象監測系統的概況來當做例子

假設系統中有三個組成要件:
(1) 氣象站: 獲取實際氣象的物理裝置,假設有三個:溫度,濕度,壓力感應
(2) Weather Data物件: 追蹤來自氣象站的資料,並且顯示在佈告版上
(3) 佈告版: 將Weather Data物件給予的資料呈現出來

整個例子會有,一個氣象站(產出假的氣象資料),Weather Data物件(獲取氣象資料並通知佈告版),佈告版將拿到的資料給呈現出來

沒使用觀察者模式 Observer Pattern的情況

初學者會很直覺的寫出這樣的程式架構:

佈告欄

CurrentConditionsDisplay.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CurrentConditionsDisplay {
private int temp;
private int humidity;
private int pressure;

public void update() {
this.temp = temp;
this.humidity = humidity;
this.pressure = pressure;
}

// 顯示資料
public void display() {
System.out.println(temp);
System.out.println(humidity);
System.out.println(pressure);
}
}

WeatherData.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class WeatherData {
// instance variable declarations

// 宣告佈告欄
CurrentConditionsDisplay currentConditionsDisplay;
StatisticsDisplay statisticsDisplay;

// 建構子
public WeatherData(
CurrentConditionsDisplay currentConditionsDisplay,
StatisticsDisplay statisticsDisplay
) {
this.currentConditionsDisplay = currentConditionsDisplay;
this.statisticsDisplay = statisticsDisplay;
}

public int getTemperature() {...}
public int getHumidity() {...}
public int getPressure() {...}

public void measurementsChanged() {
float temp = getTemperature();
float humidity = getHumidity();
float pressure = getPressure();

// 對佈告欄類別進行更新他們的顯示內容
currentConditionsDisplay.update(temp, humidity, pressure);
statisticsDisplay.update(temp, humidity, pressure);

// 其他WeatherData function...
}
}

但是以上程式結構會有耦合性的狀況:

所以接下來來了解觀察者模式的內涵

觀察者模式解析

定義了物件之間一對多關係,如此一來,當一個物件改變狀態時,其他相依者都會收到通知並自動做改變

其示意圖如下:

主題與觀察者們定義了一對多的關係

若要實踐出可以隔離主題和觀察者們的方式,以 Subject介面和Observer介面最為常見

在這張圖要注意一個重點是,由於現在已經針對介面實作,現在的Subject中的註冊Observer都是以註冊"介面"為主!而非是像上面一開始的新手例子是直接針對實踐而寫

如此一來如果要在新增一個佈告欄叫做ForecastDisplay,直接實踐Observer就好,這樣就不用動到實踐Subject介面的WeatherData之程式碼

以觀察者模式來重寫氣象監測系統

Subject.interface

1
2
3
4
5
public interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}

ObserverInterface

1
2
3
public interface Observer {
void update(float temperature, float humidity, float pressure);
}

WeatherData.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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) {
int i = observers.indexOf(o);
observers.add(o);
}

public void removeObserver(Observer o) {
int i = observers.indexOf(o);
if(i>=0) {
observers.remove(i);
}
}

// 向觀察者們送以改動的資料
// 可以看到現在我們是直接註冊Observer介面,如此一來觀察者類別的實作就不用去在意
// 只要知道要註冊的對象必須要有實作Observer介面就好
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();
}
}

這裡我們只實踐一個佈告欄 CurrentConditionDisplay

CurrentConditionDisplay.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CurrentConditionDisplay implements Observer {
private float temperature;
private float humidity;
private float pressure;
private Subject weatherData;

public CurrentConditionDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}

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

public void display() {
System.out.println("Current condition: "+temperature + "F degrees and " + humidity + "% humidity and "+ pressure + " pressure");
}
}

執行程式

1
2
3
4
5
6
7
8
9
10
public class Main {

public static void main(String[] args) {
WeatherData weatherData = new WeatherData();

CurrentConditionDisplay currentDisplay = new CurrentConditionDisplay(weatherData);

weatherData.setMeasurements(80, 64, 30.4f);
}
}

可以看到以下結果

1
Current condition: 80.0F degrees and 64.0% humidity and 30.4 pressure

之後只要透過主題呼叫觀察者的update()的方法,就可以通知新的資料給觀察者

並且透過註冊的方式+只加入針對實踐Observer介面的觀察者,如此一來可以達到分離主題物件與觀察者物件的邏輯,之後新增新的佈告欄就不用動到主題的程式邏輯。

補充

java sdk也有自行提供Observer方法

其中會有setChange()的方法,主要讓呼叫者定義什麼時候才要通知新的資料給觀察者,避免每次資料一改變就一直通知觀察者。Ex: 如果沒有setChanged的方法,WeahterData物件就會持續不斷的通知觀察者,所以若我們希望溫度差距半度才更新,溫度差距插到半度以上,主題才會呼叫觀察者的update()的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
setChanged() {
changed = ture;
}

notifyObservers(Object arg) {
if(changed) {
for every observer on the list {
call update(this. org)
}
changed = false;
}
}

notifyObsergers() {
notifyObservers(null);
}