[聚合文章] 如何在 Angular 由 Interface 注入 Object ?

Angular 2018-01-09 21 阅读

根據 依賴反轉原則 ,component 與 service,或 service 與 service 的相依,引僅限於 interface,而不該直接相依於另一個 service。但真正在 Angular 使用 interface 解耦合後,又會發現因為 JavaScript 天生沒有 interface,因此 TypeScript 與 Angular DI 必須在實務上妥協,本文整理出 3 種 Angular 官方認可 interface 注入 Object 方式。

Version

Node.js 8.9.3

Angular CLI 1.6.2

Angular 5.1.2

User Story

  • counter 初始值為 2
  • +counter + 1,按 -counter -1

Task

  • +- 放在 CounterComponent
  • counter 顯示仍在 AppComponent

Architecture

  • CounterComponent 負責 +- 的 button;而 CounterService 負責計算 counter 與保存共用的 counter,為一 BehaviorSubject
  • 根據 依賴反轉原則CounterComponent 不應該直接相依於 CounterService ,而是兩者相依於 interface
  • 根據 介面隔離原則ConterComponent 只相依於它所需要的 interface,因此以 CounterComponent 的角度訂出 ChangeCounterInterface ,且 CounterSerive 必須實踐此 interface
  • 因為 CounterComponentCounterService 都相依於 ChangeCounterInterface ,兩者都只知道 ChangeCounterInterface 而已,而不知道彼此,因此 CounterComponentCounterService 徹底解耦合
  • 透過 DI container 將實作 ChangeCounterInterfaceCounterService 注入進 CounterComponent

Implementation

change-counter.interface.ts

import { Observable } from 'rxjs/Observable';

export interface ChangeCounterInterface {
  counter$: Observable<number>;
  setInitialCount(initialValue: number);
  addOne(): void;
  minusOne(): void;
}

根據 依賴反轉原則介面隔離原則 ,我們訂出了 ChangeCounterInterface

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { CounterComponent } from './component/counter/counter.component';
import { CounterService } from './service/count/counter.service';
import { ChangeCounterInterface } from './interface/change-counter.interface';

@NgModule({
  declarations: [
    AppComponent,
    CounterComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [
    CounterService,
    {provide: ChangeCounterInterface, useExisting: CounterService}
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

AppModule 需定義 interface 與 class 的對應關係。

18 行

{provide: ChangeCounterInterface, useExisting: CounterService}

當使用 ChangeCounterInterface 時,請 DI container 幫我們注入 CounterService

其中 provide 為 token,DI container 會以此 token 為識別。

以一般使用 DI 的經驗,如此的設定完全合乎邏輯,但 Language Service 已經提出警告。

  1. 開啟 app.module.ts
  2. provide 使用的是 ChangeCounterInterface
  3. Language Service 已經提出警告,表示 ChangeCounterInterface 是個 value,而不是 type 型別

interface 在 TypeScript 的認知與傳統強型別語言不同,因為 JavaScript 沒有 interface,因此 TypeScript 的 interface 只拿來當 編譯檢查 用,不算是一個 type,而 provide 要求的是一個 type,如 class,interface 在此只當成 value 看待。

我們無法在 Angular DI 直接使用 interface 當 token,觀念上仍是 interface,但作法上必須有所妥協

String Token

Angular DI 雖然不允許我們用 interface 當 token,卻允許我們使用 string 當 token。

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { CounterComponent } from './component/counter/counter.component';
import { CounterService } from './service/count/counter.service';
import { ChangeCounterInterface } from './interface/change-counter.interface';

@NgModule({
  declarations: [
    AppComponent,
    CounterComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [
    CounterService,
    {provide: 'ChangeCounterInterface', useExisting: CounterService}
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

18 行

{provide: 'ChangeCounterInterface', useExisting: CounterService}

provide 的 token 改成 ChangeCounterInterface 字串。

app.component.ts

import { Component, Inject } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ChangeCounterInterface } from './interface/change-counter.interface';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  initialCount = 2;
  counter$: Observable<number> = this.counterService.counter$;

  constructor(@Inject('ChangeCounterInterface') private counterService: ChangeCounterInterface) {
  }
}

14 行

constructor(@Inject('ChangeCounterInterface') private counterService: ChangeCounterInterface) {
}

原本的 private counterService: ChangeCounterInterface 前面加上 @Inject() decorator,並傳入剛剛定義的 ChangeCounterInterface 字串。

原本 DI container 是以 constructor 參數的型別作為 DI 注入的依歸,但若加上 @Inject() decorator 後,會接管所有的 DI 型別,也就是改用 @Inject() 的參數作為 token。

剛剛在 AppModule 定義 ChangeCounterInterface string token,其對應的 class 為 CounterService ,因此在 CounterComponent 會將 CounterService 注入。

優點

  1. 仍然可使用 interface

缺點

  1. String token 可能重複,尤其若跟 Angular 或 3rd Party 使用相同 string token 時,將會後蓋前
  2. 在 constructor 內必須加上 @Inject() decorator,程式碼比較冗長

Injection Token

change-counter.interface.ts 仍然保持不變,也就是仍然使用 interface。

injection-token.ts

import { InjectionToken } from '@angular/core';
import { CounterInterface } from './change-counter.interface';

export const CounterInterfaceToken = new InjectionToken<CounterInterface>('');

第 4 行

export const CounterInterfaceToken = new InjectionToken<CounterInterface>('');

有鑑於 string token 可能重複,因此 Angular 4 提出 InjectToken ,簡單的說,就是由 string 換成 InjectionToken 物件,由於每次 new 的物件都不會重複,因此儘管 token 名稱相同,但實質上仍然是不同的物件。

InjectionToken 可帶泛型,也就是我們原本的 interface,初始值可帶一字串當 description,傳入空字串即可。

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { CounterComponent } from './component/counter/counter.component';
import { CounterService } from './service/count/counter.service';
import { CounterInterfaceToken } from './interface/interface-token';

@NgModule({
  declarations: [
    AppComponent,
    CounterComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [
    CounterService,
    {provide: CounterInterfaceToken, useExisting: CounterService}
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

18 行

{provide: CounterInterfaceToken, useExisting: CounterService}

provide 由 string token 改成剛剛所建立的 InjectionToken

counter.component.ts

import { Component, Inject, Input, OnInit } from '@angular/core';
import { CounterInterface } from '../../interface/change-counter.interface';
import { Observable } from 'rxjs/Observable';
import { CounterInterfaceToken } from '../../interface/interface-token';

@Component({
  selector: 'app-counter',
  templateUrl: './counter.component.html',
  styleUrls: ['./counter.component.css']
})
export class CounterComponent implements OnInit {
  @Input() initialCount;
  counter$: Observable<number> = this.counterService.counter$;

  constructor(@Inject(CounterInterfaceToken) private counterService: CounterInterface) {
  }

  ngOnInit() {
    this.counterService.setInitialCount(this.initialCount);
  }

  onIncrementClick() {
    this.counterService.addOne();
  }

  onDecrementClick() {
    this.counterService.minusOne();
  }
}

15 行

constructor(@Inject(CounterInterfaceToken) private counterService: CounterInterface) {
}

一樣使用 @Inject() decorator,只是帶入改換成 CounterInterfaceToken ,不再是字串,而是個 InjectionToken 物件。

優點

  1. 仍然可使用 interface
  2. 每個 InjectionToken 都獨一無二,不再會有後蓋前的問題

缺點

  1. 在 constructor 內必須加上 @Inject() decorator,程式碼比較冗長

Abstract Class

change-counter.interface.ts

import { Observable } from 'rxjs/Observable';

export abstract class CounterInterface {
  abstract counter$: Observable<number>;
  abstract setInitialCount(initialValue: number);
  abstract addOne(): void;
  abstract minusOne(): void;
}

將原本的 interface 寫法,全部改用 abstract class 寫法。

Interface 與 abstract class,在 OOP 的觀念上,皆屬於 抽象 ,兩者地位幾乎一樣;但在 TypeScript,interface 僅用於 編譯檢查 用,並不會實際編譯成 JavaScript,而 abstract class 卻會編譯成對應的 JavaScript。

另外在 TypeScript,class 也可以被 implement ,稱為 class interface ,這也是 TypeScript 與傳統 OOP 語言不同之處,因此我們可以借用 abstract class 當成 interface 被 service 拿去 implement

可自行到 TypeScript Playground 實際貼上 interface 與 abstract class 寫法,就會發現兩者的 JavaScript 是不一樣的

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';


import { AppComponent } from './app.component';
import { CounterComponent } from './component/counter/counter.component';
import { CounterService } from './service/count/counter.service';
import { CounterInterface } from './interface/change-counter.interface';

@NgModule({
  declarations: [
    AppComponent,
    CounterComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [
    CounterService,
    {provide: CounterInterface, useExisting: CounterService}
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

20 行

{provide: CounterInterface, useExisting: CounterService}

因為改用 abstract class 後,此時已是個 type,可光明正大在 provide 當 token 使用。

counter.component.ts

import { Component, Input, OnInit } from '@angular/core';
import { CounterInterface } from '../../interface/change-counter.interface';
import { Observable } from 'rxjs/Observable';

@Component({
  selector: 'app-counter',
  templateUrl: './counter.component.html',
  styleUrls: ['./counter.component.css']
})
export class CounterComponent implements OnInit {
  @Input() initialCount;
  counter$: Observable<number> = this.counterService.counter$;

  constructor(private counterService: CounterInterface) {
  }

  ngOnInit() {
    this.counterService.setInitialCount(this.initialCount);
  }

  onIncrementClick() {
    this.counterService.addOne();
  }

  onDecrementClick() {
    this.counterService.minusOne();
  }
}

14 行

constructor(private counterService: CounterInterface) {
}

不必使用 @Inject() decorator,DI container 會自動使用參數的型別作為 DI 注入的依據,這也於其他 OOP 語言的經驗一樣。

優點

  1. 不必使用 InjectionToken
  2. 不必使用 @Inject() decorator,用法與一般 OOP 寫法一樣

缺點

  1. 必須由 interface 改用 abstract class
  2. abstract class 可以被 implement ,與一般 OOP 的觀念不一樣

Conclusion

  • 3 種寫法都在 Angular 官網出現過,都屬官方寫法
  • 基於程式碼的精簡,個人偏好第三種寫法,也就是將 interface 改成 abstract class ,而不必使用 string token 或 InjectionToken

Sample Code

完整的範例可以在 GitHub 上找到

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