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' }],