使用 Multi Provider

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


di001

User 使用下拉選單選擇 service。

di000

共有 AWSAzureGCP 三個雲端服務可供選擇,但無論選擇哪個 service,都只能在 run-time 決定,而無法在 compile-time 決定,此時 DI 該注入哪一個 service 呢?

Task


根據 user 的需求,動態切換 service。

Architecture


di002

  • 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

di003

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

di004

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

di005

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

di006

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

di007

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

di008

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 上找到

2018-04-04