如何使用 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 上找到