Observer Pattern 是 OOP 中著名的 Design Pattern,尤其在應付 一對多
的場景特別有效,在本文中,我們將以 Angular 與 TypeScript 實現 Observer Pattern。
Version
Node.js 8.9.3
Angular CLI 1.6.2
Angular 5.2
User Story
畫面上有兩個 數字鐘
,每秒更新一次,不斷的顯示目前的時間。
Task
程式碼希望分成兩部分,一個部分是每秒送出 目前時間
,另一個部分負責 顯示時間
。
也就是一個 class 負責 產生資料
;另一 class 負責 顯示資料
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
{{ now | date:'HH:mm:ss'}}
HTML 負責顯示 目前時間
,至於 時:分:秒
部分就不用自己寫程式處理了,靠 pipe 即可。
digital-clock.component.ts
import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { ObserverInterface } from '../../interface/observer.interface'; import { SubjectInterfaceToken } from '../../interface/InjectionToken'; import { SubjectInterface } from '../../interface/subject.interface'; @Component({ selector: 'app-digital-clock', templateUrl: './digital-clock.component.html', styleUrls: ['./digital-clock.component.css'] }) export class DigitalClockComponent implements ObserverInterface, OnInit, OnDestroy { now: Date; constructor( @Inject(SubjectInterfaceToken) private clockService: SubjectInterface) { } ngOnInit(): void { this.clockService.addObserver(this); } ngOnDestroy(): void { this.clockService.removeObserver(this); } update(date: Date) { this.now = date; } }
11 行
export class DigitalClockComponent implements ObserverInterface, OnInit, OnDestroy
先不考慮 DigitalClockComponent
所使用的 ObserverInterface
,最後會討論。
回想 DigitalClockComponent
的初衷,除了顯示 目前時間
外,另外一個目的就是能對 ClockService
加以 註冊為觀察者
與 取消註冊
。
19 行
ngOnInit(): void { this.clockService.addObserver(this); }
既然要 註冊為觀察者
,要在什麼時候註冊呢 ?
最好是在 DigitalClockComponent
開始 初始化
時就對 ClockService
加以 註冊為觀察者
,因此選擇使用 ngOnInit()
lifecycle hook。
希望 ClockService
有 addObserver()
,提供 註冊為觀察者
功能。
23 行
ngOnDestroy(): void { this.clockService.removeObserver(this); }
既然有 註冊為觀察者
,就應該有 取消註冊
,該在什麼時候取消註冊呢 ?
最好是在 DigitalClockComponent
最後 被消滅
時對 ClockService
加以 取消註冊
,因此選擇 ngOnDestroy()
lifecycle hook。
希望 ClockService
有 removeObserver()
,提供 取消註冊
功能。
綜合 ngOnInit()
與 ngOnDestroy()
,根據 介面隔離原則
,已經大概可猜到 interface 該提供 addObserver()
與 removeObserver()
14 行
constructor( @Inject(SubjectInterfaceToken) private clockService: SubjectInterface) { }
既然 DigiClockComponent
需要 ClockService
,因此必須在 constructor 將 ClockService
DI 注入進來。
根據 依賴反轉原則
: DigitalClockComponent
不應該依賴底層的 ClockService
,兩者應該依賴於 interface。
根據 介面隔離原則
: DigitalClockComponent
應該只相依於他所需要的 interface,目前看來 DigitalClockComponent
需要 addObserver()
與 removeObserver()
,因此由 DigitalClockComponent
需求角度訂出的 SubjectInterface
。
因此 DI 注入的 ClockService
,其型別為 SubjectInterface
,這樣就符合 依賴反轉原則
與 介面隔離原則
。
SubjectInterface
subject.interface.ts
import { ObserverInterface } from './observer.interface'; export interface SubjectInterface { addObserver(observer: ObserverInterface): void; removeObserver(observer: ObserverInterface): void; }
根據 介面隔離原則
: DigitalClock
應該只相依於他所需要的 interface,目前看來 DigitalClock
需要 addObserver()
與 removeObserver()
,因此由 DigitalClock
需求訂出的 SubjectInterface
,應該要有 addObserver()
與 removeObserver()
。
ClockService
clock.service.ts
import { Injectable } from '@angular/core'; import { SubjectInterface } from '../../interface/subject.interface'; import { ClockInterface } from '../../interface/clock.interface'; import { ObserverInterface } from '../../interface/observer.interface'; @Injectable() export class ClockService implements SubjectInterface { private observers: ObserverInterface[] = []; constructor() { setInterval(() => this.tick(), 1000); } addObserver(observer: ObserverInterface): void { this.observers.push(observer); } removeObserver(observer: ObserverInterface): void { const index = this.observers.indexOf(observer); if (index === -1) { return; } this.observers.splice(index, 1); } private tick(): void { this.observers.forEach(observer => observer.update(new Date())); } }
第 7 行
export class ClockService implements SubjectInterface
根據 依賴反轉原則
, ClockService
應該相依於 DigitalClockComponent
所訂出的 interface,因此必須實現 SubjectInterface
。
第 8 行
private observers: ObserverInterface[] = [];
observers
陣列儲存所有註冊為 觀察者
的物件,每個物件型別為 ObserverInterface
。
也就是只要有實作 ObserverInterface
的物件,都算是 觀察者
,至於 ObserverInterface
是什麼 ? 稍後會討論
14 行
addObserver(observer: ObserverInterface): void { this.observers.push(observer); } removeObserver(observer: ObserverInterface): void { const index = this.observers.indexOf(observer); if (index === -1) { return; } this.observers.splice(index, 1); }
既然 SubjectInterface
已經定義了 addObserver()
與 removeObserver()
, ClockService
就應該時做出 addObserver()
與 removeObserver()
。
10 行
constructor() { setInterval(() => this.tick(), 1000); }
根據需求, ClockService
要能夠每秒送出 目前時間
,所以使用 JavaScript 原生的 setInterval()
,每 1 秒鐘呼叫 tick()
一次。
28 行
private tick(): void { this.observers.forEach(observer => observer.update(new Date())); }
每一秒執行 tick()
時,會將 observers
陣列全部跑一遍,執行陣列內每個 觀察者
update()
。
根據 介面隔離原則
, ClockService
訂出 ObserverInterface
,且必須要有 update()
。
ObserverInterface
observer.interface.ts
export interface ObserverInterface { update(date: Date); }
根據 介面隔離原則
: ClockService
應該只相依於他所需要的 interface,目前看來 ClockService
需要 update()
,因此由 ClockService
需求訂出的 ObserverInterface
,所有 觀察者
都應該具備 ObserverInterface
。
DigitalClockComponent
digital-clock.component.ts
import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { ObserverInterface } from '../../interface/observer.interface'; import { SubjectInterfaceToken } from '../../interface/InjectionToken'; import { SubjectInterface } from '../../interface/subject.interface'; @Component({ selector: 'app-digital-clock', templateUrl: './digital-clock.component.html', styleUrls: ['./digital-clock.component.css'] }) export class DigitalClockComponent implements ObserverInterface, OnInit, OnDestroy { now: Date; constructor( @Inject(SubjectInterfaceToken) private clockService: SubjectInterface) { } ngOnInit(): void { this.clockService.addObserver(this); } ngOnDestroy(): void { this.clockService.removeObserver(this); } update(date: Date) { this.now = date; } }
回到一開始還沒討論完的 DigitalClockComponent
。
11 行
export class DigitalClockComponent implements ObserverInterface
DigitalClockComponent
因為要對 ClockService
加以 註冊為觀察者
,所以是個典型的 觀察者
,因此要實現 ObserverInterface
。
27 行
update(date: Date) { this.now = date; }
因為 ObserverInterface
有定義 update()
,因此要實作出 update()
,也就是將 ClockService
送出的目前時間 date
,指定給 now
,準備 data binding 顯示。
AppModule
app.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { SubjectInterfaceToken } from './interface/InjectionToken'; import { ClockService } from './service/clock/clock.service'; import { DigitalClockComponent } from './component/digital-clock/digital-clock.component'; @NgModule({ declarations: [ AppComponent, DigitalClockComponent ], imports: [ BrowserModule ], providers: [ ClockService, {provide: SubjectInterfaceToken, useExisting: ClockService} ], bootstrap: [AppComponent] }) export class AppModule { }
既然在 DigitalClockComponent
使用 DI 注入了 ClockService
,就必須在 provider
交代 interface 與 service 的關係。
16 行
providers: [ ClockService, {provide: SubjectInterfaceToken, useExisting: ClockService} ],
當使用 SubjectInterfaceToken
,注入 ClockService
。
由於我們希望無論有幾個 觀察者
,都顯示 相同時間
,因此 ClockService
將採用 Singleton 方式,所以使用 useExisting
。
AppComponent
折騰了這麼久,到底使用 Observer Pattern 的威力在哪裡 ?
app.component.html
<app-digital-clock></app-digital-clock> <p></p> <app-digital-clock></app-digital-clock>
在 AppComponent
使用了 2 個 DigitalClockComponent
,都會自己去註冊 ClockService
,且無論使用 n 的 DigitalClockComponent
,都不用改程式碼,達到 開放封閉原則
的要求。
Summary
Observer Pattern
當物件之間有 一對多
的依賴關係,且當 一
的 主題
(subject) 改變時, 多
的 觀察者
(observer) 也必須跟著改變,此時就適合使用 Observer Pattern
此為 Observer Pattern 最原始的 UML class diagram, 主題
(subject) 與 觀察者
(observer) 彼此互相依賴,為了不讓 subject 與 object 之間強耦合,採用了 依賴反轉原則
與 介面隔離原則
,分別訂出 SubjectInterface
與 ObserverInterface
,讓 subject 與 object 彼此僅相依於對方訂出的 interface,如此 主題
就不須知道有多少 觀察者
正在觀察,且 觀察者
也不須知道實際的 主題
為何, 觀察者
只關心 主題
能不能被 註冊
, 主題
只關心 觀察者
能不能被 通知
,如此 主題
與 觀察者
就徹底 解耦合
了,且將來無論新增多少 觀察者
, 主題
與 觀察者
的程式碼都不用修改,符合 開放封閉原則
的要求。
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 上找到
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。