Angular 提供 DI,讓我們能以依賴注入的方式將 servie 注入,若我們無法在 compile-time 就決定要注入的 service,一直要到 run-time 才能決定,我們該如何在 run-time 動態切換 service 呢?
Version
macOS High Sierra 10.13.4
WebStorm 2018.1
Node.js 8.9.4
Angular CLI 1.7.3
Angular 5.2.8
User Story
User 使用下拉選單選擇 service。
共有 AWS
、Azure
與 GCP
三個雲端服務可供選擇,但無論選擇哪個 service,都只能在 run-time 決定,而無法在 compile-time 決定,此時 DI 該注入哪一個 service 呢?
Task
根據 user 的需求,動態切換 service。
Architecture
- AppComponent:Angular 的 component
- ServiceFactory:負責將 user 選擇的 service 傳回,使用 DI 注入進 component
- ServiceInterface:所有 service 共同的 interface
- AWSService:負責
AWS
功能的 service
- AzureService:負責
Azure
功能的 service
- GCPService:負責
GCP
功能的 service
Implementation
app.component.html
1 2 3 4
| <select (change)="onChange()" #mySelect> <option *ngFor="let service of services" [value]="service.name">{{ service.name }}</option> </select> <p>{{ message }}</p>
|
在 HTML 提供 <select>
選擇 service,結果會顯示在 message
。
AppComponent
app.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import { Component, ElementRef, ViewChild } from '@angular/core'; import { Service } from './models/service.model'; import { ServiceInterface } from './interfaces/service.interface'; import { ServiceFactory } from './services/service.factory'; import { ServiceEnum } from './enums/service.enum';
@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { services: Service[] = [ { name: ServiceEnum.NULL }, { name: ServiceEnum.AWS }, { name: ServiceEnum.Azure }, { name: ServiceEnum.GCP } ];
message = 'Please select service';
@ViewChild('mySelect') private mySelect: ElementRef; private service: ServiceInterface;
constructor(private serviceFactory: ServiceFactory) {}
onChange() { const serviceName = this.mySelect.nativeElement.value; this.service = this.serviceFactory.createService(serviceName); this.message = this.service.getMessage(); } }
|
24 行
1
| private service: ServiceInterface;
|
重點在 service
無法由 compile-time 決定,要由 user 在 run-time 選擇,因此 constructor 要 DI 注入什麼則面臨挑戰。
26 行
1
| constructor(private serviceFactory: ServiceFactory) {}
|
既然無法在 compile-time 決定要注入什麼 service,那就改注入 ServiceFactory
,由 不變
的 ServiceFactory
幫我們決定 變動
的 service。
30 行
1 2
| this.service = this.serviceFactory.createService(serviceName); this.message = this.service.getMessage();
|
由 ServiceFactory.createService()
根據 user 在 run-time 所選擇的 service 名稱,回傳正確的 service。
AppComponent
改注入 ServiceFactory
後,最少 AppComponent
已經符合 開放封閉原則
,將來無論新增任何 service,AppComponent
都不用再做任何修改
ServiceFactory
service.factory.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { ServiceInterface } from '../interfaces/service.interface'; import { ServiceToken } from '../tokens/service.token'; import { Inject, Injectable } from '@angular/core'; import { ServiceEnum } from '../enums/service.enum';
@Injectable() export class ServiceFactory { constructor(@Inject(ServiceToken) private services: ServiceInterface[]) { }
createService(name: ServiceEnum): ServiceInterface { return this.services .filter(item => item.getName() === name)[0]; } }
|
第 8 行
1 2
| constructor(@Inject(ServiceToken) private services: ServiceInterface[]) { }
|
將符合 ServiceInterface
的所有物件都注入,注意 services
的型別是 ServiceInterface[]
。
該如何一次注入所有實現 ServiceInterface
的 service 呢?會在 AppModule
動手腳,稍後會說明。
11 行
1 2 3 4
| createService(name: ServiceEnum): ServiceInterface { return this.services .filter(item => item.getName() === name)[0]; }
|
因為 services
包含所有的 service,使用 filter()
找到符合 user 選擇的 service 並回傳。
ServiceInterface
service.interface.ts
1 2 3 4 5 6
| import { ServiceEnum } from '../enums/service.enum';
export interface ServiceInterface { getName(): ServiceEnum; getMessage(): string; }
|
所有 service 共同遵守的 interface,共有 getName()
與 getMessage()
兩個 method。
AWSService
aws.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { Injectable } from '@angular/core'; import { ServiceInterface } from '../interfaces/service.interface'; import { ServiceEnum } from '../enums/service.enum';
@Injectable() export class AWSService implements ServiceInterface { getName(): ServiceEnum { return ServiceEnum.AWS; }
getMessage(): string { return 'You have selected AWS service'; } }
|
AWSService
實現 ServiceInterface
所定義的 method。
AzureService
azure.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { Injectable } from '@angular/core'; import { ServiceInterface } from '../interfaces/service.interface'; import { ServiceEnum } from '../enums/service.enum';
@Injectable() export class AzureService implements ServiceInterface { getName(): ServiceEnum { return ServiceEnum.Azure; }
getMessage(): string { return 'You have selected Azure service'; } }
|
AzureService
實現 ServiceInterface
所定義的 method。
GCPService
gcp.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { Injectable } from '@angular/core'; import { ServiceInterface } from '../interfaces/service.interface'; import { ServiceEnum } from '../enums/service.enum';
@Injectable() export class GcpService implements ServiceInterface { getName(): ServiceEnum { return ServiceEnum.GCP; }
getMessage(): string { return 'You have selected GCP service'; } }
|
GcpService
實現 ServiceInterface
所定義的 method。
AppModule
app.module.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { AWSService } from './services/aws.service'; import { ServiceToken } from './tokens/service.token'; import { AzureService } from './services/azure.service'; import { GcpService } from './services/gcp.service'; import { ServiceFactory } from './services/service.factory'; import { NullService } from './services/null.service';
@NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule ], providers: [ ServiceFactory, { provide: ServiceToken, useClass: NullService, multi: true}, { provide: ServiceToken, useClass: AWSService, multi: true}, { provide: ServiceToken, useClass: AzureService, multi: true}, { provide: ServiceToken, useClass: GcpService, multi: true} ], bootstrap: [AppComponent] }) export class AppModule { }
|
20 行
1 2 3 4
| { provide: ServiceToken, useClass: NullService, multi: true}, { provide: ServiceToken, useClass: AWSService, multi: true}, { provide: ServiceToken, useClass: AzureService, multi: true}, { provide: ServiceToken, useClass: GcpService, multi: true}
|
該如何讓符合 ServiceInterface
的 service 在 ServiceFactory
一次全部注入呢 ? 關鍵就在 AppModule
的 provider 加上 multi: true
,則所有相同 interface 的 service 將以 array 的方式一次全部注入。
Summary
AppComponent
由原本注入 service 改注入 ServiceFactory
- 由
ServiceFactory
將所有相同 interface 的 service 全部注入
- 由
ServiceFactory.CreateService()
決定要 filter()
哪個 service 給 client
- 將來若有新的 service 新增,只需繼續根據
ServiceInterface
新增 service,並在 AppModule
補上 provider,其他程式碼都不必修改,符合 開放封閉原則
的要求
Conclusion
- 本文的做法,與原本動態 DI 注入的理想還有段距離,但藉由在相同 token 的 multi provider,讓相同 interface 的所有 service 都能一起注入在
ServiceFactory
,這樣最少 component 符合 開放封閉原則
,將來維護也方便
Sample Code
完整的範例可以在我的 GitHub 上找到