跳至主要内容

[GoF] 觀察者模式 Observer

此篇為各筆記之整理,非原創內容,資料來源主要為:

TL;DR

觀察者模式,當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知。

概念

在沒有使用觀察者模式前,如果想要追蹤某個物件的狀態是否有發生變化時,必須不斷的去詢問與查詢該物件狀態;但透過觀察者模式,一旦主體(subject)發生改變(或其他合適的時機),它會主動通知所有訂閱該主題的訂閱者。

舉例來說,如果有用過 react-hook-form 這個套件的話,如果想要知道表單最新的資料狀態時,可以主動使用 getValues('firstName') 來取出表單的值;但如果我們時希望表單一有更新就通知我們的話,則可以使用它所提供的 watch 方法,這個 watch('firstName') 的使用,就是觀察者模式,但 watch 的對象的資料狀態發生改變時,它就會主動通知元件,並得到最新的資料狀態。

名詞解釋

observable(又稱 subject)通常會具有下列功能和元素:

  • observer:通常是可以被執行的「函式」,或者是物件中帶有可被執行的函式。當收到通知(notification)時,會觸發執行 observer。又常稱作 subscriberlistener
  • subscribe():用來把 observer 加入 observers 陣列中的方法
  • unsubscribe():用來把 observer 從 observers 陣列中移除的方法
  • notify():一旦呼叫此方法,則所有該 observable 中的 observers 都會被執行。又常稱作 dispatch()broadcast()
提示

observable (subject) 可以想成是帶有上述這些元素的一個東東。

常見的觀察者模式

DOM 事件

實際上,很常使用到的 addEventListener 就類似 observer pattern:

btn.addEventListener('click', () => {
// ...
});
  • onClick 中的 callback function 就類似 observer pattern 中的 observer
  • addEventListener 就類似 observer pattern 中的 subscribe()
  • btn 就像是 subject

Redux

Redux Store 最基本的程式邏輯也是利用 observer pattern 來通知所有訂閱到該資料的元件要 re-render。可以參考官方文件 Inside a Redux Store

實作:自定義事件

在不考慮效能或其他功能的情況下,observer pattern 基本的實作並不複雜:

// https://www.patterns.dev/posts/observer-pattern/
class Observable {
constructor() {
// 把所有的 observers 都放在這個陣列中
this.observers = [];
}

// 透過 subscribe 把 callback function 保存在 observers 中
subscribe(func) {
this.observers.push(func);

// 需要的話也可以回傳一個 unsubscribe function
return () => {
const index = this.observers.indexOf(func);
this.observers.splice(index, 1);
};
}

// 或者另外提供 unsubscribe 的 function
unsubscribe(func) {
this.observers = this.observers.filter((observer) => observer !== func);
}

// notify 執行時,所有 observers 中 function 都會被執行
notify(data) {
this.observers.forEach((observer) => observer(data));
}
}

使用

function logger(data) {
console.log(`${Date.now()} ${data}`);
}

function greet(data) {
console.log(`Hi ${data}`);
}

// 建立一個 subject
const observable$ = new Observable();

// 透過 subscribe 把 fn 保存在 observers 中
const unsubscribeLogger = observable$.subscribe(logger);
const unsubscribeGreet = observable$.subscribe(greet);

// 呼叫該 subject 的 notify
observable$.notify('Aaron');

console.log('--- unsubscribe ---');
unsubscribeLogger();

observable$.notify('Aaron');

// 1648304648618 Aaron
// Hi Aaron
// --- unsubscribe ---
// Hi Aaron

實作:訂閱不同的主題

如果有需要的話,也可以讓使用者訂閱及接受不同主題的內容,只需要將原本的 observers 由 observer[],變成一個 Map<string, observer[]> 的資料結構即可,實際的實作可以參考 observer pattern @ pjchender GitHub。

發佈/訂閱(Publish/Subscribe, pubsub)

PubSub 將觀察者模式做了泛型化,解決 Observer Pattern 中耦合和效能的問題。

PubSub 透過在 Publisher 和 Subscriber 中間添加了 Channel 層,來達到減少耦合。

參考資料

Giscus