Design Pattern 在 Angular 將有全新的使用方式

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


observer000

畫面上有兩個 數字鐘,每秒更新一次,不斷的顯示目前的時間。

Task


程式碼希望分成兩部分,一個部分是每秒送出 目前時間,另一個部分負責 顯示時間

也就是一個 class 負責產生資料;另一 class 負責 顯示資料

Definition


Observer Pattern

當物件之間有 一對多 的依賴關係,且當 主題 (subject) 改變時,觀察者 (observer) 也必須跟著改變,此時就適合使用 Observer Pattern

observer007

此為 Observer Pattern 最原始的 UML class diagram,主題 (subject) 與 觀察者 (observer) 彼此互相依賴,為了不讓 subject 與 object 之間強耦合,採用了 依賴反轉原則介面隔離原則,分別訂出 SubjectInterfaceObserverInterface,讓 subject 與 object 彼此僅相依於對方訂出的 interface,如此 主題 就不須知道有多少 觀察者 正在觀察,且 觀察者也不須知道實際的 主題 為何,觀察者 只關心 主題 能不能被 註冊主題 只關心 觀察者 能不能被 通知,如此 主題觀察者 就徹底 解耦合了,且將來無論新增多少 觀察者主題觀察者 的程式碼都不用修改,符合 開放封閉原則 的要求。

Architecture


observer001

  • ClockService 負責 產生資料,也就是每秒送出 目前時間
  • DigitalClockComponent 負責 顯示資料,也就是顯示 目前時間
  • DigitalClockComponent 必須能向 ClockService 註冊為觀察者,根據 DigitalClockComponent 的需求,訂出 SubjectInterface,期望 ClockService 能遵守
  • ClockService 必須能向 DigitalClockComponent 每秒送出 目前時間,根據 ClockService 的需求,訂出 ObserverInterface,期望 DigitalClockComponent 能遵守

Implementation


DigitalClockComponent

observer002

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 行

1
export class DigitalClockComponent implements ObserverInterface, OnInit, OnDestroy

先不考慮 DigitalClockComponent 所使用的 ObserverInterface ,最後會討論。

19 行

1
2
3
ngOnInit(): void {
this.clockService.addObserver(this);
}

回想 DigitalClockComponent 的初衷,除了顯示 目前時間 外,另外一個目的就是能對 ClockService 加以 註冊為觀察者取消註冊

既然要 註冊為觀察者,要在什麼時候註冊呢 ?

最好是在 DigitalClockComponent 開始 初始化 時就對 ClockService 加以 註冊為觀察者,因此選擇使用 ngOnInit() lifecycle hook。

希望 ClockServiceaddObserver(),提供 註冊為觀察者 功能。

23 行

1
2
3
ngOnDestroy(): void {
this.clockService.removeObserver(this);
}

既然有 註冊為觀察者,就應該有 取消註冊,該在什麼時候取消註冊呢 ?

最好是在 DigitalClockComponent 最後 被消滅 時對 ClockService 加以 取消註冊,因此選擇 ngOnDestroy() lifecycle hook。

希望 ClockServiceremoveObserver() ,提供 取消註冊 功能。

綜合 ngOnInit()ngOnDestroy(),根據 介面隔離原則,已經大概可猜到 interface 該提供 addObserver()removeObserver()

14 行

1
2
3
4
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

observer002

subject.interface.ts

1
2
3
4
5
6
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

observer005

clock.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 行

1
export class ClockService implements SubjectInterface

根據 依賴反轉原則ClockService 應該相依於 DigitalClockComponent 所訂出的 interface,因此必須實現 SubjectInterface

第 8 行

1
private observers: ObserverInterface[] = [];

observers 陣列儲存所有註冊為 觀察者 的物件,每個物件型別為 ObserverInterface

也就是只要有實作 ObserverInterface 的物件,都算是 觀察者,至於 ObserverInterface 是什麼 ? 稍後會討論

14 行

1
2
3
4
5
6
7
8
9
10
11
12
13
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 行

1
2
3
constructor() {
setInterval(() => this.tick(), 1000);
}

根據需求,ClockService 要能夠每秒送出 目前時間,所以使用 JavaScript 原生的 setInterval(),每 1 秒鐘呼叫 tick() 一次。

28 行

1
2
3
private tick(): void {
this.observers.forEach(observer => observer.update(new Date()));
}

每一秒執行 tick() 時,會將 observers 陣列全部跑一遍,執行陣列內每個 觀察者 update()

根據 介面隔離原則ClockService 訂出 ObserverInterface,且必須要有 update()

ObserverInterface

observer006

observer.interface.ts

1
2
3
export interface ObserverInterface {
update(date: Date);
}

根據 介面隔離原則 : ClockService 應該只相依於他所需要的 interface,目前看來 ClockService 需要 update(),因此由 ClockService 需求訂出的 ObserverInterface,所有 觀察者 都應該具備 ObserverInterface

DigitalClockComponent

observer002

digital-clock.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 行

1
export class DigitalClockComponent implements ObserverInterface

DigitalClockComponent 因為要對 ClockService 加以 註冊為觀察者,所以是個典型的 觀察者,因此要實現 ObserverInterface

27 行

1
2
3
update(date: Date) {
this.now = date;
}

因為 ObserverInterface 有定義 update(),因此要實作出 update(),也就是將 ClockService 送出的目前時間 date,指定給 now,準備 data binding 顯示。

AppModule

app.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 行

1
2
3
4
providers: [
ClockService,
{provide: SubjectInterfaceToken, useExisting: ClockService}
],

當使用 SubjectInterfaceToken,注入 ClockService

由於我們希望無論有幾個 觀察者,都顯示 相同時間,因此 ClockService 將採用 Singleton 方式,所以使用 useExisting

AppComponent

折騰了這麼久,到底使用 Observer Pattern 的威力在哪裡 ?

app.component.html

1
2
3
<app-digital-clock></app-digital-clock>
<p></p>
<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 上找到