根據 依賴反轉原則 ,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 - 因為
CounterComponent與CounterService都相依於ChangeCounterInterface,兩者都只知道ChangeCounterInterface而已,而不知道彼此,因此CounterComponent與CounterService徹底解耦合 - 透過 DI container 將實作
ChangeCounterInterface的CounterService注入進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 已經提出警告。
- 開啟
app.module.ts -
provide使用的是ChangeCounterInterface - 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 注入。
優點
- 仍然可使用
interface
缺點
- String token 可能重複,尤其若跟 Angular 或 3rd Party 使用相同 string token 時,將會後蓋前
- 在 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 物件。
優點
- 仍然可使用
interface - 每個
InjectionToken都獨一無二,不再會有後蓋前的問題
缺點
- 在 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 語言的經驗一樣。
優點
- 不必使用
InjectionToken - 不必使用
@Inject()decorator,用法與一般 OOP 寫法一樣
缺點
- 必須由
interface改用abstract class -
abstract class可以被implement,與一般 OOP 的觀念不一樣
Conclusion
- 3 種寫法都在 Angular 官網出現過,都屬官方寫法
- 基於程式碼的精簡,個人偏好第三種寫法,也就是將
interface改成abstract class,而不必使用 string token 或InjectionToken
Sample Code
完整的範例可以在 GitHub 上找到
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。