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 上找到
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。