循環参照 ~Service同士のDIで気をつけるべきこと~~


Serviceとは

AngularのServiceは、Componentでデータの受け渡しに集中するために、委譲されたロジックの部分の責務をもたせるというデザインパターンの一つです。
これによって、Componentのなかをシンプルにすることができます。

import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { HttpClient, HttpHeaders } from '@angular/common/http';

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

  private heroesUrl = 'api/heroes';

  constructor(
    private http: HttpClient
  ) { }

  public getHero(id: number): Observable<Hero> {
    const url = `${this.heroesUrl}/${id}`;
    return this.http.get<Hero>(url)
      .pipe(
        tap(_ => this.log(`fetched hero id=${id}`)),
        catchError(this.handleError<Hero>(`getHero id=${id}`))
      );
  }

}

  • Componentの記述がシンプルになり、保守性が高まります
  • Componentで、Serviceを使うときはDI(Dependency Injection)をすることになります。(参考記事はこちら)
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.scss']
})
export class HeroesComponent implements OnInit {

  public heroes: Hero[];

  constructor(
    private heroService: HeroService
  ) { }

  ngOnInit() {
    this.getHeroes();
  }

  private getHeroes(): void {
    this.heroService.getHeroes()
      .subscribe(heroes => this.heroes = heroes);
  }

}

Service同士でDependencyInjectionしてみる

Service同士でDependency Injectionすることも可能です。

LoggerService

log関連のメソッドをもつService

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { HttpClient, HttpHeaders } from '@angular/common/http';

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

  constructor(
  ) { }

  private add(message: string) {
    console.log(` ${message}`);
  }
}

LoggerServiceをHeroServiceにDependencyInjectionする

errorが出たとき、logを出力します。
logを出力するというメソッドは、LoggerServiceに委譲するようにします。
これは,あるServiceからあるServiceへの一方向のDIとなります。

import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { loggerService } from './logger.service';
import { HttpClient, HttpHeaders } from '@angular/common/http';

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

  private heroesUrl = 'api/heroes';

  constructor(
    private http: HttpClient,
    private loggerService: LoggerService,
    private messageService: Message
  ) { }

  public getHero(id: number): Observable<Hero> {
    const url = `${this.heroesUrl}/${id}`;
  private messageService: MessageService,
    return this.http.get<Hero>(url)
      .pipe(
        tap(_ => this.logggerService.add(`fetched hero id=${id}`)),
        catchError(this.handleError<Hero>(`getHero id=${id}`))
      );
  }

}

循環参照を起こすDIの仕方

では、相互間のDIの場合、どうなるのでしょうか。

MessageService

heroServiceのgetHeroメソッドをaddUrlメソッドで使用しています。

import { Injectable } from '@angular/core';
import { Message } from './message';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { heroService } from './hero.service';
import { loggerService } from './logger.service';
import { HttpClient, HttpHeaders } from '@angular/common/http';

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

  constructor(
    private http: HttpClient,
    private heroService: HeroService,
    private loggerService: LoggerService
  ) { }

  public addUrl(url: string): Observable<Message> {
    this.heroService.getHero.subscribe(hero => {
     const message = `この${url}では、${hero}が取得できます`;
     this.loggerService.add(message);
     )
  }

}

HeroService

messageServiceのaddUrlメソッドをgetHeroメソッドで使用しています。

import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { messageService } from './message.service';
import { loggerService } from './logger.service';
import { HttpClient, HttpHeaders } from '@angular/common/http';

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

  private heroesUrl = 'api/heroes';

  constructor(
    private http: HttpClient,
    private messageService: MessageService,
    private loggerService: LoggerService
  ) { }

  public getHero(id: number): Observable<Hero> {
    const url = `${this.heroesUrl}/${id}`;
    this.messageService.addUrl(url);
    return this.http.get<Hero>(url)
      .pipe(
        tap(_ => this.loggerService.add(`fetched hero id=${id}`)),
        catchError(this.handleError<Hero>(`getHero id=${id}`))
      );
  }

}

上記では、MessageServiceでHeroServiceの返り値を取得してるが、その取得するためにはMessageServiceの返り値を取得しなければいけないという循環参照に陥っています。
当然ながらerrorが吐き出されます。

Uncaught Error: Can't resolve all parameters for HeroService: (?, [object Object], [object Object]).
    at syntaxError (webpack-internal:///306:684)
    at CompileMetadataResolver._getDependenciesMetadata (webpack-internal:///306:15764)
    at CompileMetadataResolver._getTypeMetadata (webpack-internal:///306:15599)
    at CompileMetadataResolver._getInjectableMetadata (webpack-internal:///306:15579)
    at CompileMetadataResolver.getProviderMetadata (webpack-internal:///306:15939)
    at eval (webpack-internal:///306:15850)
    at Array.forEach (<anonymous>)
    at CompileMetadataResolver._getProvidersMetadata (webpack-internal:///306:15810)
    at CompileMetadataResolver.getNgModuleMetadata (webpack-internal:///306:15378)
    at CompileMetadataResolver.getNgModuleSummary (webpack-internal:///306:15216)

注意するべきこと

このように相互間でのServiceは循環参照のerrorを起こす可能性があるので、注意してServiceをDependencyInjectionするべきです。

解決策

これへの解決策として中間Serviceを挟むなどがあります。
HogeServiceとFooServiceで循環参照を起こさないようにするために、MetaServiceを経由して、HogeServiceとFooServiceをやり取りするなどがあります。

参考記事