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つ目の実装クラス。
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によるサポートが受けられ、実装漏れもなくなる。上記の例は 'project'
というstringリテラルをDI tokenとして 'A'
, 'B'
という値が 'root'
でprovideされている前提で実装している。なので、AppModuleのprovidersでこの値をprovideしておく。
app.module.ts// この場合TestServiceをDIするとTestAServiceが使われる providers: [{ provide: 'project', useValue: 'A' }],