AngularでServiceの実装クラス差し替え
投稿日: 12/29/2021
はじめに
AngularでServiceを作っているときに、共通の外部Interface(実体がありDIさせる)の元で状況に応じて内部実装を差し替えたい場合がある。こういうときの一つの方法としてStrategyパターンでServiceを実装すると良いという話があって、実際自分も試してみて良かったので内容をまとめておく。
元ネタは以下のng-japan onairの動画(36:30辺り)
実装
これから以下のような状況を仮定して実装する。
TestService:外部Interface。利用者はこのServiceをDIする。ライブラリとして提供する場合はこのServiceだけを
public-apiに載せて、続く2つの実装クラスは基本的に公開しないようにする。そして、実装の切り替えは'root'でprovideする値'A','B'によって行う。
TestAService:
'A'がprovideされたときに使用されるTestServiceの1つ目の実装クラス。
TestBService:
'B'がprovideされたときに使用されるTestServiceの2つ目の実装クラス。
外部Interface
test-service.tsimport { Injectable } from '@angular/core';
import { TestAService } from './test-a.service';
import { TestBService } from './test-b.service';
export type Project = 'A' | 'B';
@Injectable({
providedIn: 'root',
useFactory: (
project: Project,
testAService: TestAService,
testBService: TestBService
) => {
switch (project) {
case 'A':
return testAService;
case 'B':
return testBService;
}
},
deps: ['project', TestAService, TestBService],
})
export abstract class TestService {
abstract greeting(): string;
}useFactoryでprojectの値に応じて、TestAServiceもしくはTestBServiceを返すようにしている。ここではTestAService,TestBServiceはシングルトンという前提で、newせずdepsで既に存在しているインスタンスを利用するようにしている。もちろんnewすることも可能。TestServiceは外部Interfaceとして存在するだけなので、抽象クラスで実装し、メソッドも抽象メソッドとして定義する。
実装クラス
ほぼ同一の実装だが、一応 TestAService と TestBService の両方を載せておく。
test-a.service.tsimport { Injectable } from '@angular/core';
import { TestService } from './test.service';
@Injectable({
providedIn: 'root',
})
export class TestAService implements TestService {
greeting(): string {
return 'This is TestAService';
}
}test-b.service.tsimport { Injectable } from '@angular/core';
import { TestService } from './test.service';
@Injectable({
providedIn: 'root',
})
export class TestBService implements TestService {
greeting(): string {
return 'This is TestBService';
}
}implements TestServiceでTestServiceをInterfaceとして利用する。これによりTSによるサポートが受けられ、実装漏れもなくなる。
条件のProvide (今回の実装特有の事情)
上記の例は 'project' というstringリテラルをDI tokenとして 'A', 'B' という値が 'root' でprovideされている前提で実装している。なので、AppModuleのprovidersでこの値をprovideしておく。
app.module.ts// この場合TestServiceをDIするとTestAServiceが使われる
providers: [{ provide: 'project', useValue: 'A' }],