如何在 Angular 由 Interface 注入 Service ?
根據 依賴反轉原則 ,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
1 | import { Observable } from 'rxjs/Observable'; |
根據 依賴反轉原則 與 介面隔離原則,我們訂出了 ChangeCounterInterface。
app.module.ts
1 | import { BrowserModule } from '@angular/platform-browser'; |
在 AppModule 需定義 interface 與 class 的對應關係。
18 行
1 | {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
1 | import { BrowserModule } from '@angular/platform-browser'; |
18 行
1 | {provide: 'ChangeCounterInterface', useExisting: CounterService} |
將 provide 的 token 改成 ChangeCounterInterface 字串。
app.component.ts
1 | import { Component, Inject } from '@angular/core'; |
14 行
1 | 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
1 | import { InjectionToken } from '@angular/core'; |
第 4 行
1 | export const CounterInterfaceToken = new InjectionToken<CounterInterface>(''); |
有鑑於 string token 可能重複,因此 Angular 4 提出 InjectToken,簡單的說,就是由 string 換成 InjectionToken 物件,由於每次 new 的物件都不會重複,因此儘管 token 名稱相同,但實質上仍然是不同的物件。
InjectionToken 可帶泛型,也就是我們原本的 interface,初始值可帶一字串當 description,傳入空字串即可。
app.module.ts
1 | import { BrowserModule } from '@angular/platform-browser'; |
18 行
1 | {provide: CounterInterfaceToken, useExisting: CounterService} |
provide 由 string token 改成剛剛所建立的 InjectionToken。
counter.component.ts
1 | import { Component, Inject, Input, OnInit } from '@angular/core'; |
15 行
1 | constructor(@Inject(CounterInterfaceToken) private counterService: CounterInterface) { |
一樣使用 @Inject() decorator,只是帶入改換成 CounterInterfaceToken,不再是字串,而是個 InjectionToken 物件。
優點
- 仍然可使用
interface - 每個
InjectionToken都獨一無二,不再會有後蓋前的問題
缺點
- 在 constructor 內必須加上
@Inject()decorator,程式碼比較冗長
Abstract Class
change-counter.interface.ts
1 | import { Observable } from 'rxjs/Observable'; |
將原本的 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
1 | import { BrowserModule } from '@angular/platform-browser'; |
20 行
1 | {provide: CounterInterface, useExisting: CounterService} |
因為改用 abstract class 後,此時已是個 type,可光明正大在 provide 當 token 使用。
counter.component.ts
1 | import { Component, Input, OnInit } from '@angular/core'; |
14 行
1 | 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上找到