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.ts
import { 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; }
  • useFactoryproject の値に応じて、TestAService もしくは TestBService を返すようにしている。ここでは TestAService, TestBService はシングルトンという前提で、new せず deps で既に存在しているインスタンスを利用するようにしている。もちろん new することも可能。
  • TestService は外部Interfaceとして存在するだけなので、抽象クラスで実装し、メソッドも抽象メソッドとして定義する。

実装クラス

ほぼ同一の実装だが、一応 TestAServiceTestBService の両方を載せておく。

test-a.service.ts
import { 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.ts
import { Injectable } from '@angular/core'; import { TestService } from './test.service'; @Injectable({ providedIn: 'root', }) export class TestBService implements TestService { greeting(): string { return 'This is TestBService'; } }
  • implements TestServiceTestService を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' }],