如何在 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上找到