如何使用 TypeScript 實現 Observer Pattern ?
Observer Pattern 是 OOP 中著名的 Design Pattern,尤其在應付 一對多 的場景特別有效,在本文中,我們將以 Angular 與 TypeScript 實現。
Version
Node.js 8.9.3
Angular CLI 1.6.2
TypeScript 2.5.3
Angular 5.2
User Story

畫面上有兩個 數字鐘,每秒更新一次,不斷的顯示目前的時間。
Task
程式碼希望分成兩部分,一個部分是每秒送出 目前時間,另一個部分負責 顯示時間。
也就是一個 class 負責
產生資料;另一 class 負責顯示資料
Definition
Observer Pattern
當物件之間有
一對多的依賴關係,且當一的主題(subject) 改變時,多的觀察者(observer) 也必須跟著改變,此時就適合使用Observer Pattern
此為 Observer Pattern 最原始的 UML class diagram,主題 (subject) 與 觀察者 (observer) 彼此互相依賴,為了不讓 subject 與 object 之間強耦合,採用了 依賴反轉原則 與 介面隔離原則,分別訂出 SubjectInterface 與 ObserverInterface,讓 subject 與 object 彼此僅相依於對方訂出的 interface,如此 主題 就不須知道有多少 觀察者 正在觀察,且 觀察者也不須知道實際的 主題 為何,觀察者 只關心 主題 能不能被 註冊,主題 只關心 觀察者 能不能被 通知,如此 主題 與 觀察者 就徹底 解耦合了,且將來無論新增多少 觀察者,主題 與 觀察者 的程式碼都不用修改,符合 開放封閉原則 的要求。
Architecture
ClockService負責產生資料,也就是每秒送出目前時間DigitalClockComponent負責顯示資料,也就是顯示目前時間DigitalClockComponent必須能向ClockService註冊為觀察者,根據DigitalClockComponent的需求,訂出SubjectInterface,期望ClockService能遵守ClockService必須能向DigitalClockComponent每秒送出目前時間,根據ClockService的需求,訂出ObserverInterface,期望DigitalClockComponent能遵守
Implementation
DigitalClockComponent
DigitalClockComponent 負責 顯示資料,既然是 顯示資料,在 Angular 最適合的並不是 service,而是 component,而且 Angular 的 component 已經 class 化,表示所有 OOP 能用的技巧都能使用,因此決定使用 component 實作 。
digital-clock.component.html
1 | {{ now | date:'HH:mm:ss'}} |
HTML 負責顯示 目前時間,至於 時:分:秒 部分就不用自己寫程式處理了,靠 pipe 即可。
digital-clock.component.ts
1 | import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; |
11 行
1 | export class DigitalClockComponent implements ObserverInterface, OnInit, OnDestroy |
先不考慮 DigitalClockComponent 所使用的 ObserverInterface ,最後會討論。
19 行
1 | ngOnInit(): void { |
回想 DigitalClockComponent 的初衷,除了顯示 目前時間 外,另外一個目的就是能對 ClockService 加以 註冊為觀察者 與 取消註冊 。
既然要 註冊為觀察者,要在什麼時候註冊呢 ?
最好是在 DigitalClockComponent 開始 初始化 時就對 ClockService 加以 註冊為觀察者,因此選擇使用 ngOnInit() lifecycle hook。
希望 ClockService 有 addObserver(),提供 註冊為觀察者 功能。
23 行
1 | ngOnDestroy(): void { |
既然有 註冊為觀察者,就應該有 取消註冊,該在什麼時候取消註冊呢 ?
最好是在 DigitalClockComponent 最後 被消滅 時對 ClockService 加以 取消註冊,因此選擇 ngOnDestroy() lifecycle hook。
希望 ClockService 有 removeObserver() ,提供 取消註冊 功能。
綜合
ngOnInit()與ngOnDestroy(),根據介面隔離原則,已經大概可猜到 interface 該提供addObserver()與removeObserver()
14 行
1 | constructor( |
既然 DigiClockComponent 需要 ClockService,因此必須在 constructor 將 ClockService DI 注入進來。
根據 依賴反轉原則 : DigitalClockComponent 不應該依賴底層的 ClockService,兩者應該依賴於 interface。
根據 介面隔離原則 : DigitalClockComponent 應該只相依於他所需要的 interface,目前看來 DigitalClockComponent 需要 addObserver() 與 removeObserver(),因此由 DigitalClockComponent 需求角度訂出的 SubjectInterface。
因此 DI 注入的 ClockService ,其型別為 SubjectInterface,這樣就符合 依賴反轉原則 與 介面隔離原則。
SubjectInterface
subject.interface.ts
1 | import { ObserverInterface } from './observer.interface'; |
根據 介面隔離原則 : DigitalClock 應該只相依於他所需要的 interface,目前看來 DigitalClock 需要 addObserver() 與 removeObserver(),因此由 DigitalClock 需求訂出的 SubjectInterface,應該要有 addObserver() 與 removeObserver()。
ClockService
clock.service.ts
1 | import { Injectable } from '@angular/core'; |
第 7 行
1 | export class ClockService implements SubjectInterface |
根據 依賴反轉原則,ClockService 應該相依於 DigitalClockComponent 所訂出的 interface,因此必須實現 SubjectInterface。
第 8 行
1 | private observers: ObserverInterface[] = []; |
observers 陣列儲存所有註冊為 觀察者 的物件,每個物件型別為 ObserverInterface。
也就是只要有實作
ObserverInterface的物件,都算是觀察者,至於ObserverInterface是什麼 ? 稍後會討論
14 行
1 | addObserver(observer: ObserverInterface): void { |
既然 SubjectInterface 已經定義了 addObserver() 與 removeObserver() ,ClockService 就應該時做出 addObserver() 與 removeObserver()。
10 行
1 | constructor() { |
根據需求,ClockService 要能夠每秒送出 目前時間,所以使用 JavaScript 原生的 setInterval(),每 1 秒鐘呼叫 tick() 一次。
28 行
1 | private tick(): void { |
每一秒執行 tick() 時,會將 observers 陣列全部跑一遍,執行陣列內每個 觀察者 update()。
根據 介面隔離原則, ClockService 訂出 ObserverInterface,且必須要有 update()。
ObserverInterface
observer.interface.ts
1 | export interface ObserverInterface { |
根據 介面隔離原則 : ClockService 應該只相依於他所需要的 interface,目前看來 ClockService 需要 update(),因此由 ClockService 需求訂出的 ObserverInterface,所有 觀察者 都應該具備 ObserverInterface。
DigitalClockComponent
digital-clock.component.ts
1 | import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; |
回到一開始還沒討論完的 DigitalClockComponent。
11 行
1 | export class DigitalClockComponent implements ObserverInterface |
DigitalClockComponent 因為要對 ClockService 加以 註冊為觀察者,所以是個典型的 觀察者,因此要實現 ObserverInterface。
27 行
1 | update(date: Date) { |
因為 ObserverInterface 有定義 update(),因此要實作出 update(),也就是將 ClockService 送出的目前時間 date,指定給 now,準備 data binding 顯示。
AppModule
app.module.ts
1 | import { BrowserModule } from '@angular/platform-browser'; |
既然在 DigitalClockComponent 使用 DI 注入了 ClockService,就必須在 provider 交代 interface 與 service 的關係。
16 行
1 | providers: [ |
當使用 SubjectInterfaceToken,注入 ClockService。
由於我們希望無論有幾個 觀察者,都顯示 相同時間,因此 ClockService 將採用 Singleton 方式,所以使用 useExisting。
AppComponent
折騰了這麼久,到底使用 Observer Pattern 的威力在哪裡 ?
app.component.html
1 | <app-digital-clock></app-digital-clock> |
在 AppComponent 使用了 2 個 DigitalClockComponent,都會自己去註冊 ClockService,且無論使用 n 的 DigitalClockComponent,都不用改程式碼,達到 開放封閉原則 的要求。
Conclusion
- 傳統後端 MVC 架構,Design pattern 大都用在 service,少部分機會可以用在 repository,但 controller 與 view 就很難用上,尤其是 view 幾乎與 Design Pattern 絕緣;但 Angular 將 component 給 class 化之後,又能使用 interface,使的 view 也有使用 Design Pattern 的可能。本文就是使用 component 直接當
Observer使用,這使的 Design Pattern 在 Angular 應用上有全新的可能,不再只限於 service,連 component 也可以使用
Sample Code
完整的範例可以在我的 GitHub 上找到