深入淺出設計模式(Design Pattern)-觀察者模式(2-Observer Pattern)

Ben
生活的各種可能性
15 min readJan 23, 2024
photo by Sergey Semin

嗨,又見到你了,應該是你吧?我跟照片都凝視著你,就是這種感覺。這篇就是要來介紹觀察者模式(Observer Pattern)

那我們就直接開始吧

觀察者模式(Observer Pattern)

首先我們先來看看書中對他的定義:定義物件之間的一對多依賴關係,當一個物件改變狀態時,依賴他的物件都會自動收到通知與更新。

一對多關係示意圖

什麼意思呢?讓我們來看看這次的範例

情境:我們要建構一個網路氣象觀測站,這個觀測站必須使用一個WeatherData物件來建構,而這個物件可以追蹤目前的天氣狀況(氣溫、濕度、氣壓)。我們的目標是建構一個在WeatherData更新時同時更新的氣象顯示應用程式。

首先我們釐清一下我們的目標:

  1. 在WeatherData物件中我們有三個方法可以取得目前最新的資料(getTemperature、getHumidity、getPressure),但我們並不在乎他怎麼取得的,我們只知道他可以取得最新的資料。
  2. 我們知道WeatherData中當值被更新的時候,measurementsChanged這個方法就會被呼叫。
  3. 我們必須在WeatherData被更新的時候更新我們的應用程式,而且為了更新畫面,我們必須在measurementsChanged方法中加入程式碼。
  4. 同時我們還收到畫面必須可以自訂,這也代表不會只有一個畫面,我們也要把擴展性考慮進去。
// 提供資訊的對象介面
interface Subject {
registerObserver(obs: Observer): void;
removeObserver(index: number): void;
notifyObservers(): void;
}

// 觀察者介面
interface Observer {
update(): void;
}

// 顯示介面
interface DisplayElement {
display(): void;
}

// 實作天氣對象
class WeatherData<T extends Observer> implements Subject {
private observers: T[] = [];
private temperature: number = 0;
private humidity: number = 0;
private pressure: number = 0;

// 註冊觀察者
public registerObserver(obs: T): void {
this.observers.push(obs);
}

// 移除觀察者
public removeObserver(index: number): void {
this.observers.splice(index, 1);
}

// 提醒所有觀察者更新
public notifyObservers(): void {
this.observers.forEach((obs: T) => {
obs.update();
});
}

// 資料更新完
public measurementsChanged(): void {
this.notifyObservers();
}

// 更新資料
public setMeasurements(temperature: number, humidity: number, pressure: number) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
this.measurementsChanged();
}

public get observersList(): T[] {
return this.observers;
}

public get getTemperature(): number {
return this.temperature;
}

public get getHumidity(): number {
return this.humidity;
}

public get getPressure(): number {
return this.pressure;
}
}

// 實作觀察者1
class WeatherObserver1Display implements Observer, DisplayElement {
private temperature: number = 0;
private humidity: number = 0;
private pressure: number = 0;
private weatherData: WeatherData<Observer>;

constructor(weatherData: WeatherData<Observer>) {
// 初始化天氣對象
this.weatherData = weatherData;
// 向對象註冊自己
this.weatherData.registerObserver(this);
}

// 依據自己要的資料內容向對象拉取更新後的資料(對象提供getter)
public update(): void {
this.temperature = this.weatherData.getTemperature;
this.humidity = this.weatherData.getHumidity;
this.pressure = this.weatherData.getPressure;
this.display();
}

public display(): void {
console.log(`Observer1 Current temperature: ${this.temperature}, humidity: ${this.humidity}, pressure: ${this.pressure}`);
}
}

class WeatherObserver2Display implements Observer, DisplayElement {
private temperature: number = 0;
private humidity: number = 0;
private pressure: number = 0;
private weatherData: WeatherData<Observer>;

constructor(weatherData: WeatherData<Observer>) {
// 初始化天氣對象
this.weatherData = weatherData;
// 向對象註冊自己
this.weatherData.registerObserver(this);
}

// 依據自己要的資料內容向對象拉取更新後的資料(對象提供getter)
public update(): void {
this.temperature = this.weatherData.getTemperature;
this.humidity = this.weatherData.getHumidity;
this.pressure = this.weatherData.getPressure;
this.display();
}

public display(): void {
console.log(`Observer2 Current temperature: ${this.temperature}, humidity: ${this.humidity}, pressure: ${this.pressure}`);
}
}

class WeatherObserver3Display implements Observer, DisplayElement {
private temperature: number = 0;
private humidity: number = 0;
private pressure: number = 0;
private weatherData: WeatherData<Observer>;

constructor(weatherData: WeatherData<Observer>) {
// 初始化天氣對象
this.weatherData = weatherData;
// 向對象註冊自己
this.weatherData.registerObserver(this);
}

// 依據自己要的資料內容向對象拉取更新後的資料(對象提供getter)
public update(): void {
this.temperature = this.weatherData.getTemperature;
this.humidity = this.weatherData.getHumidity;
this.pressure = this.weatherData.getPressure;
this.display();
}

public display(): void {
console.log(`Observer3 Current temperature: ${this.temperature}, humidity: ${this.humidity}, pressure: ${this.pressure}`);
}
}

// 氣象站台,當對象更新資料時,所有觀察者都會被同步通知
class WeatherStation {
weatherData: WeatherData<Observer> = new WeatherData();
wheaterObserver1Display: WeatherObserver1Display = new WeatherObserver1Display(this.weatherData);
wheaterObserver2Display: WeatherObserver2Display = new WeatherObserver2Display(this.weatherData);
wheaterObserver3Display: WeatherObserver3Display = new WeatherObserver3Display(this.weatherData);

constructor() {
this.weatherData.setMeasurements(80, 65, 30);
this.weatherData.setMeasurements(82, 70, 29);
this.weatherData.setMeasurements(78, 90, 10);
}
}

const wheaterStations = new WeatherStation();

上方的範例中,我們一共有三個抽象的介面,分別是被觀察者(Subject)、觀察者(Observer)、顯示(DisplayElement)。

抽象介面(Subject、Observer、DisplayElement)

Subject(interface):首先要成為被觀察對象就必須實作Subject這個介面,所以我們的被觀察對象WeatherData就必須實作這個介面的內容,而Subject提供了註冊觀察者(registerObserver)、移除觀察者(removeObserver)、通知觀察者(notifyObservers)這三個抽象的方法。

Observer(interface):同樣的要成為觀察者就必須實作Observer這個介面,這個介面則提供了update方法。

DisplayElement(interface):這個介面可以讓你自訂你要顯示的方式,同樣的你也必須實作它。

實例化(WeatherData、WeatherObserverDisplay)

WeatherData(class):我們來看看比較核心的部分。在WeatherData中我們除了有我們的要顯示的氣象資料外,我們還會有一個observer的陣列,他是來存放觀察者們的陣列。而我們實作註冊的方式(registerObserver)則是接受一個Observer類型的實例的參數並把它加到observer陣列當中,而當資料更新時我們將會提醒(notifyObservers)所有的觀察者,我們會遍歷observers中所有有被註冊的觀察者,並調用他們擁有的方法:upadte。

WeatherObserver1Display(class):他實現了Observer以及DisplayElement兩個介面,在類別當中則包了儲存WeatherData中資料狀態的初始值,以及擁有一個實例化的WeatherData的值,並且在建構式中接收並初始化,初始化後在建構式中調用WeatherData中註冊(registerObserver)方法向被觀察對象(WeatherData)註冊自己成為觀察者。

這邊是最重要的部分,我們將自己告訴對方要成為觀察者,所以當被觀察者有所變化的時候就會通知所有有註冊成為觀察者的對象,我們就可以即時更新狀態。

怎麼更新的?

還記得這個class我們必須實作的介面(Observer)嗎,我們必須在這個class中實作update方法,也就是我們可以自己定義我們需要的資料有哪些,以及如何更新等等細節。

接下來當WeatherData資料有更新的時候將會調用measurementsChanged這個方法,而measurementsChanged則是調用notifyObservers這個方法,於是他就會遍歷WeatherData中observers陣列中所有的觀察者並且調用他們更新的方法(update)告訴他們必須更新資料。這也是為什麼每個想成為觀察者的人都必須實作Observer這個介面,這個介面也同時提供一個共同參考給Subject(WeatherData)和Observer(WeatherObserverDisplay)。

鬆耦合的威力

觀察者模式是很棒的鬆耦合典範,我們來看看這個模式是怎麼實現鬆耦合的。

  1. Subject不需要知道Observer的具體類別、功能,還有關於它的任何事情。
  2. 我們可以隨時加入新的觀察者,而且完全不需要修改Subject。我們只需要在新的類別裡實作Observer介面並將它註冊為觀察者即可。
  3. 我們可以重複使用Subject或Observer,又不會影響彼此。
  4. 以任何方式修改Subject或Observer都不會影響另一方。因為它們是鬆耦合的,所以我們可以放心的修改任一方,只要物件仍然善盡它們的義務,實作Subject或Observer介面即可。

設計原則:努力為彼此互動的物件做出鬆耦合的設計。

鬆耦合的設計可以讓我們做出靈活的、可以處理變動的物件導向系統,因為它可以將物件之間的相互依賴性降到最低。

觀察者模式與設計原則

  1. 找出程式中會變的部分,把它們和不會變的部分隔開:在觀察者模式中,會變的是Subject的狀態,以及觀察者的數量和型態。在這種模式中,當你改變依賴Subject狀態的物件時,不需要改變Subject。
  2. 針對介面寫程式,而不是針對實作寫程式:Subject和觀察者都使用介面。Subject會記住實作了觀察者介面的物件,而觀察者會像Subject註冊,並且收到通知。這種做法可以讓程式井井有條,並且保持鬆耦合。
  3. 多用組合,少用繼承:觀察者模式使用組合來將任何數量的觀察者與他們的Subject組合起來。這些關係不是用繼承階層來安排的,而是在執行期用組合來設置的。

重點提示

接下來我們來看看這個章節中重要的重點提示:

  • 觀察者模式定義了物件之間一對多的關係。
  • Subject會用共同的介面來更新觀察者。
  • 任何一種具體型態的觀察者都可以加入這種模式,只要他們有實作觀察者介面即可。
  • 觀察者是鬆耦合的,因為Subject對他們一無所知,只知道他們都實作了觀察者介面。
  • 在這種模式中,你可以從Subject推送資料,或是讓觀察者拉取資料(一般認為推送比較正統)。
  • 觀察者模式與發佈 / 訂閱模式有關,後者是在比較複雜的情況下使用的,它有更多Subject與(或)多種訊息類型。
  • 觀察者模式是常用的模式。

結語

觀察者模式是一個蠻有趣也讓我收穫很多的模式,它讓我了觀察者模式的思維是如何切分物件之間的職責以及他們組合交互後的行為是怎麼達成鬆耦合、擴充、複用性等等在程式設計中重要的事情,同時還完成了情境的需求。是一個既優雅、簡潔又充滿藝術的設計方式,接下來我們還會介紹更多。

下一篇要介紹:裝飾器模式(Decorator Pattern)

這篇就到這邊瞜,如果有任何問題歡迎留言給我,喜歡的話也別忘了clap起來唷。

--

--