中安拓也のブログ

プログラミングについて書くブログ。 Twitterやってます @takuya_nakayasu

【Angular】useExistingで一つのサービスインスタンスに複数のDIトークンを紐づける

はじめに

あるDIトークンを別のDIトークンにマッピングすることができる、プロバイダーキーuseExistingについて説明します。

基本的なプロバイダー設定

useExistingについて触れる前に、基本的なプロバイダーの設定についておさらいしていきます。

下記のようにプロバイダーを設定すると、一件のSampleServiceインスタンスが作成され、DIトークンSampleServiceに関連づけられます。

@Injectable({
  providedIn: 'root',
})
export class SampleService {}

もしくは

providers: [SampleService]

なお、providers: [SampleService]は、[{ provide: SampleService, useClass: SampleService }]のシンタックスシュガーのため、どちらも同じ意味になります。

上記でプロバイダー設定されているSampleServieをコンポーネントやサービスクラスに注入するときは、以下のようにプロバイダーで指定されているDIトークンSampleServiceの型を使用する必要があります。

constructor(heroService: HeroService)

上記の記法で、SampleServieインスタンスを複数のDIトークン(SampleServiceBasicService)に紐づけようとすると下記のような書き方になると思います。

  providers: [
    SampleService,
    { provide: BasicService, useClass: SampleService },
  ],

また、上記で定義したサービスをコンポーネントやサービスに注入しようとすると下記のような書き方になります。

  constructor(
    private sampleService: SampleService,
    private basicService: BasicService
  ) {}

上記の書き方だと、確かにSampleServieインスタンスは複数のDIトークン(SampleServiceBasicService)に紐づきますが、その代わりSampleServieインスタンスが複数件(上記の例だと2件)作成されます。

シングルトンとして使用されることを意図したサービスのインスタンスを複数件作成してしまった場合、期待結果通りにシステムが動かなくなることがあります。*1

このように、一件のサービスインスタンスに複数のDIトークンを紐づけたい場合には、useExistingが役に立ちます。

useExistingについて

プロバイダーキーのuseExistingは、あるDIトークンを別のDIトークンにマッピングすることができるため、一つのサービスインスタンスに複数のDIトークンを紐づける、といった使い方ができます。

例えば、SampleServiceを2件のDIトークン(SampleServiceBasicService)に紐づける場合には、下記のような書き方ができます。

  providers: [
    SampleService,
    { provide: BasicService, useExisting: SampleService },
  ],

上記の書き方は、useClassを使った下記の書き方と同様の意味になります。

  providers: [
    { provide: SampleService, useClass: SampleService },
    { provide: BasicService, useExisting: SampleService },
  ],

上記のようにuseExistingを使用してプロバイダー設定を書くことで、SampleServiceのインスタンスを複数件作成せずに、SampleServiceのインスタンスを複数のDIトークンに紐づけることができます。

流れとしては、下記のようになります。

  1. { provide: SampleService, useClass: SampleService }により、SampleServiceのインスタンスが1件作成されたあと、DIトークンSampleServiceに割り当てられる

  2. useExistingを使うことでDIトークンSampleServiceをDIトークンBasicServiceにマッピングする

  3. 結果として、2件のDIトークン(SampleServiceBasicService)が同じ1件のSampleServiceインスタンスに関連付けられる

使用例

useExistingの具体的な使用例として、カスタムエラーハンドラーを直接呼び出す時に使ったりします。

カスタムエラーハンドラーを直接呼び出す

Angularでカスタムエラーハンドラーを定義するには、以下のような設定をする必要があります。

@Injectable()
export class SampleErrorHandler implements ErrorHandler {
  public handleError(error: any): void {
    console.error(error);
  }

  public openErrorWindow(): void {
    window.alert('エラーが発生しました');
  }
}

@NgModule({
  providers: [{ provide: ErrorHandler, useClass: SampleErrorHandler }]
})
class SampleModule {}

この状態でカスタムエラーハンドラーSampleErrorHandlerのメソッドopenErrorWindow()を下記のようにコンポーネントから呼び出そうとするとNullInjectorErrorエラーで失敗します。

export class SampleComponent {
  constructor(private sampleErrorHandler: SampleErrorHandler) {
    this.sampleErrorHandler.openErrorWindow();
  }
}

発生するエラー

NullInjectorError: R3InjectorError(SampleModule)[SampleErrorHandler -> SampleErrorHandler -> SampleErrorHandler]: 
  NullInjectorError: No provider for SampleErrorHandler!

上記のようにNullInjectorErrorが発生するのは、SampleErrorHandlerインスタンスがDIトークンErrorHandlerにしか関連付けられていないのに、コンポーネントのSampleComponentでDIトークンSampleErrorHandlerからSampleErrorHandlerインスタンスを参照しようとしているのが原因です。

ここで、useExistingを使って下記のように設定すると、エラーを発生させずにカスタムエラーハンドラーのメソッドを直接呼ぶことができるようになります。

@Injectable()
export class SampleErrorHandler implements ErrorHandler {
  public handleError(error: any): void {
    console.error(error);
  }

  public openErrorWindow(): void {
    window.alert('エラーが発生しました');
  }
}

@NgModule({
  providers: [
    { provide: ErrorHandler, useClass: SampleErrorHandler },
    { provide: SampleErrorHandler, useExisting: ErrorHandler },
  ],
})
class SampleModule {}

@Component({
  selector: 'sample-root',
  templateUrl: './sample.component.html',
  styleUrls: ['./sample.component.scss'],
})
export class SampleComponent {
  constructor(private sampleErrorHandler: SampleErrorHandler) {
    this.sampleErrorHandler.openErrorWindow();
  }
}

上記では、useExistingにより、カスタムエラーハンドラーSampleErrorHandlerのインスタンスが2件のDIトークン(ErrorHandlerSampleErrorHandler)に紐づけられることで、コンポーネントSampleComponentからカスタムエラーハンドラーのopenErrorWindow()メソッドを呼び出すことができるようになっています。

参考サイト

https://angular.jp/guide/dependency-injection-in-action

https://angular.jp/guide/singleton-services

*1:サービスが持っているプロパティが期待結果通り更新されないなどの意図しない動作が発生する可能性がある