[聚合文章] 如何使用 Angular 實現 Observer Pattern ?

JavaScript 2018-01-17 13 阅读

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。

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

23 行

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

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

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

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

綜合 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 之間強耦合,採用了 依賴反轉原則介面隔離原則 ,分別訂出 SubjectInterfaceObserverInterface ,讓 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 上找到

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