ウェブサイト検索

NestJSにおける循環依存性の理解


著者は「Write for DOnations」プログラムの一環として寄付を受けるために、Free and Open Source Fundを選びました。

紹介

NestJSは、モジュール間の循環依存性を検出する度にこのエラーをスローします。このチュートリアルでは、NestJSにおける循環依存性とは何か、そしてそれをどのように解決できるかを学びます。

循環依存は、2つのクラスが互いに依存しているときに発生します。NestJSでは、モジュールとプロバイダーの間で循環依存が発生する可能性があります。循環依存にはさまざまなタイプがあり、開発中によく遭遇する一般的なものは次のとおりです。

  1. 循環モジュールインポート - モジュールAがモジュールBをインポートし、モジュールBがモジュールAをインポートする場合。
  2. 循環コンストラクタ - サービスAがサービスBを注入し、サービスBが再びサービスAを注入する場合。
  3. 循環ファイルインポート - ファイルAがファイルBをインポートし、逆もまた同様です。

この記事では、OrdersModuleとPaymentModuleの2つのモジュールからなるデモのeコマースアプリケーションを扱います。これらの2つのモジュールには、それぞれのプロバイダーであるOrdersServiceとPaymentServiceがあります。OrderServiceは注文の支払いプロセスを開始し、PaymentServiceは支払いが処理された後に注文のステータスを更新する必要があります。これにより、モジュールとサービスのレベルで循環依存関係が生じます。双方のリソースが互いに依存しているからです。

注意: あなたのNestプロジェクトをブートストラップするには、次のコマンドを実行してください。

nest new circular-dependency

次に、nest g resource orders と nest g resource payment を実行して、モジュール、コントローラー、およびサービスファイルを作成する注文と支払いの新しいリソースを生成します。

前提条件

このチュートリアルに従うためには、次のものが必要です:

  • あなたのオペレーティングシステムにNode.js(バージョン >= 12)をインストールするには、Node.jsのインストール方法とローカル開発環境の作成に従ってください。
  • VSCodeのようなコードエディタ。
  • NestJSにおける依存性注入の仕組み、プロバイダー、コントローラー、モジュールに関する前提知識を持っていること。NestJSにおける依存性注入に関するこのガイドを参照してください。

モジュール間の循環依存性

まず、OrdersModuleとPaymentModuleが互いに依存している状況で発生するエラーを詳しく見てみましょう。

Nest cannot create the PaymentModule instance.
The module at index [0] of the PaymentModule "imports" array is undefined.

NestJSでは、モジュールは互いに依存することができますが、2つのモジュールが互いを直接インポートしている場合、循環依存が発生し、NestJSはそれを解決できません。これは、各モジュールが他のモジュールが完全に初期化されるのを待っているため、自分自身を初期化できず、デッドロックが発生することを意味します。

NestJSがアプリケーションを起動すると、最初にすべてのモジュールとその依存関係を解決し、その後にモジュールのインスタンスを作成します。次に、NestJSはルートモジュールapp.module.tsのインポート配列を調べ、OrderModuleの解決を始めます。そして、PaymentModuleに依存していることを確認します。NestJSは、PaymentModuleがOrdersModuleのインポート配列にあるため、この依存関係を最初に解決するためにOrdersModuleの解決を一時停止します。NestJSがPaymentModuleに切り替えると、インポートを確認し、OrdersModuleがリストにあることがわかります。 OrdersModule がまだ完全に解決されていないことに注意してください。NestJS は、PaymentModule を解決する過程で、OrdersModule の初期化を試みます。これにより、OrdersModule の初期化と解決に戻り、OrdersModule が PaymentsModule の初期化を待っており、PaymentModule が OrdersModule の初期化を待っているため、ループに陥ります。

仮にPaymentModuleがOrdersModuleの唯一の依存関係であり、その逆ではないとすると、上記のようにPaymentModuleを解決する際、NesJSはそのインポート配列を確認し、他のモジュールを解決し、OrderModuleがそのインポート配列に含まれていないため、PaymentModuleをインスタンス化します。

モジュール間の循環依存性エラーの解決

モジュールの前方参照

モジュール間の循環依存関係を解決するために、OrderModuleとPaymentModuleの両方にforwardRefユーティリティ関数を使用します。

import { Module, forwardRef } from "@nestjs/common";
import { PaymentService } from "./payment.service";
import { PaymentController } from "./payment.controller";
import { OrdersModule } from "../orders/orders.module";

@Module({
  imports: [forwardRef(() => OrdersModule)],
  controllers: [PaymentController],
  providers: [PaymentService],
  exports: [PaymentService],
})
export class PaymentModule {}
import { Module, forwardRef } from "@nestjs/common";
import { OrdersService } from "./orders.service";
import { OrdersController } from "./orders.controller";
import { PaymentModule } from "../payment/payment.module";

@Module({
  imports: [forwardRef(() => PaymentModule)],
  controllers: [OrdersController],
  providers: [OrdersService],
  exports: [OrdersService],
})
export class OrdersModule {}

コードを保存し、ターミナルを再起動してください。これでエラーは解消されているはずです:

上記のコードでは、forwardRefが両側のインポートをラップするために使用され、両方のモジュールが互いの依存関係を即座に解決することなく読み込まれることを保証します。

バックエンドでは、NestJSがモジュール初期化プロセスを開始し、モジュールのインポート配列にforwardRefが見られると、両方のモジュールをすぐに解決しようとはせず、依存関係を認識するだけで解決を遅らせます。OrdersModuleとPaymentModuleは、デッドロックを防ぐために依存関係が完全に解決されることなく、アプリケーションのコンテキストに読み込まれます。すべてのモジュールが読み込まれた後、NestJSは保留された依存関係に戻ります。現在、OrdersModuleとPaymentModuleは読み込まれ(完全に初期化されていない)、NestJSはすべての依存関係を解決するためにforwardRef関数を呼び出します。

サービス間の循環依存エラーの解決

前のセクションでは、モジュールレベルで発生する循環依存関係を解決する方法を学びました。二番目のシナリオは、OrdersService で注文を作成し、それをデータベースに保存した後、すぐに processPayment メソッドを呼び出してユーザーの注文を処理するというものです。そして、PaymentService では、processPayment メソッドが呼び出されると、すぐに updateOrderStatus メソッドを呼び出し、注文のステータスを successful または任意のステータスに更新します。

これらの依存関係を両方のサービスに注入してください:

interface IOrder {
  id: number;
  customerName: string;
  item: string;
  orderDate: Date;
  totalAmount: number;
}
@Injectable()
export class OrdersService {
  constructor(private readonly paymentService: PaymentService) {}

      async getAllOrders(): Promise<IOrder[]> {
    const mockOrders: IOrder[] = [
      {
        id: 1,
        customerName: 'Taofiq',
        item: 'Airpod',
        orderDate: new Date(),
        totalAmount: 900,
      },
    ];
    return mockOrders;
  }

  async createOrder(createOrderDTO: CreateOrderDTO): Promise<any> {
    // mock data to simulate order creation
    const newOrder: any = {
      id: Math.floor(Math.random() * 100) + 1,
      ...createOrderDTO,
      orderDate: new Date(createOrderDTO.orderDate),
    };

    await this.paymentService.processPayment(newOrder.id);
    return newOrder;
  }
  async updateOrderStatus(orderId: string, status: string) {
    // update the order status here in the database
    console.log(`Order ${orderId} status updated to ${status}`);
  }
}
@Injectable()
export class PaymentService {
  constructor(private readonly ordersService: OrdersService) {}
  async processPayment(orderId: string) {
    // In a real scenario, you would interact with a payment gateway.
    console.log(`Processing payment for order ${orderId}`);
    const paymentSuccessful = true;

    if (paymentSuccessful) {
      // Once payment is successful, update the order status to "Paid"
      await this.ordersService.updateOrderStatus(orderId, 'Paid');
    }
  }
}

これらのファイルを保存してサーバーを再起動すると、このエラーが発生します:

このエラーは、NestJSの依存性注入システムコンテナがOrdersServiceとPaymentServiceの直接的な循環依存関係を解決できないために発生します。

アプリが起動すると、両方のモジュールがインスタンス化され、登録されます。その後、NestJSはPaymentServiceのインスタンスを作成しようとし、依存関係としてOrdersServiceのインスタンスが必要であることを確認します。NestJSはPaymentModuleのスコープ内にOrdersServiceのインスタンスが存在するかをチェックします。これにより、サービスレベルでの循環依存が発生します。

これを解決するために、サービスのコンストラクタで @Inject デコレーターを forwardRef 関数と組み合わせて使用することができます。これにより、NestJSは依存関係の解決を遅らせることができ、forwardRef がモジュールレベルで機能するのと同じように動作します。

@Injectable()
export class OrdersService {
  constructor(
    @Inject(forwardRef(() => PaymentService))
    private readonly paymentService: PaymentService,
  ) {}
@Injectable()
export class PaymentService {
  constructor(
    @Inject(forwardRef(() => OrdersService))
    private readonly ordersService: OrdersService,
  ) {}
}

@Inject(forwardRef(() => ServiceName))を使用することにより、NestJSのDIコンテナは、指定されたサービスの解決を必要になるまで延期します。これにより、OrdersServiceとPaymentServiceの両方が、もう一方が完全に解決される必要なくインスタンス化できるようになります。これにより、サービスレベルでの循環依存が解消され、NestJSが依存関係を正常に注入できるようになります。

代替ソリューションとしての共有モジュール

あなたのEコマースアプリケーションでは、ユーザーが満足していない場合に注文の返金を要求できる返金プロセス機能を持ちたいと仮定します。この機能は、注文モジュールと支払いモジュールの両方が関与します。

  • OrderServiceは、注文の返金対象を確認する必要があります(例:返品の期間や、すでに返金されているかどうか)。
  • PaymentServiceは、統合することにした任意の支払いゲートウェイを通じて返金処理を行い、OrderServiceに対して注文のステータスを返金済みに更新するよう指示する必要があります。

これにより、OrderServiceが払い戻しを処理するためにPaymentServiceに依存し、PaymentServiceが払い戻しの状態を更新するためにOrderServiceに依存するため、循環依存関係のケースが発生します。

代わりに、forwardRef 関数で発生する可能性のある循環依存関係を解決するのではなく、RefundManagementModule と呼ばれる共用モジュールを導入することによって、互いに依存するサービスを分離することができます。このモジュールは、以下のように返金プロセスを調整します:

import { Injectable } from '@nestjs/common';
import { OrderService } from './order.service';
import { PaymentService } from './payment.service';

@Injectable()
export class RefundManagementService {
  constructor(
    private orderService: OrderService,
    private paymentService: PaymentService
  ) {}

  async processRefund(orderId: string) {
    const eligible = await this.orderService.checkRefundEligibility(orderId);
    if (!eligible) {
      throw new Error('Order not eligible for refund');
    }

    const refundSuccessful = await this.paymentService.processRefund(orderId);
    if (refundSuccessful) {
      await this.orderService.updateOrderStatus(orderId, 'Refunded');
    }
  }
}

ここでは、checkRefundEligibilityが注文の返金適格性を確認します。適格基準を満たす場合、PaymentServiceのprocessRefundメソッドを使用して、その注文の返金を処理します。このサービスは、RefundManagementModule内でエクスポートされ、各OrdersServiceおよびPaymentModuleにインポートされて使用できます。これにより、2つのモジュールが互いに依存することはありません。

ヒント: サーバーアプリケーションを作成する際には、循環依存関係をできるだけ避けるべきです。循環依存関係に気づいた場合は、アプリケーションのアーキテクチャをリファクタリングし、以前使用したforwardRefメソッドのような遅延評価器を必要としないコードを分割する方法を考えてみてください。モジュールコンテキスト内の2つのサービスが必要とする単一のメソッドがある場合は、それを共有フォルダーに移動し、独自のプロバイダーとユーティリティタイプのモジュールを持たせることを検討できます。

Madgeを使用して循環依存性を検出する

アプリケーション内の循環依存関係は、Madgeという開発者ツールを使用して検出できます。Madgeは、モジュールの依存関係の視覚的なグラフを生成し、コード内の循環依存関係を見つけます。

madgeを依存関係としてインストールするには、npm i madgeまたはyarn add madgeを使用し、次のコマンドを実行します:

npx madge --circular src/main.ts

または

yarn madge --circular src/main.ts

circular/.circular()は、循環依存関係を持つすべてのモジュールの配列を返します。このパッケージを使用する際に呼び出すことができる他のメソッドについて学ぶには、こちらを訪れてください。

コマンドを実行すると、ターミナルに自分のサークラー ファイルがどこにあるかを示す出力が表示されるはずです。

@Injectデコレーターは、NestJSのDIシステムの基本的なコンポーネントです。その主な機能は、通常はプロバイダーであるクラスに注入される依存関係を明示的に定義することです。

結論

この記事では、NestJSにおける循環依存性の理解、種類、およびそれを解決するためのベストプラクティスについて深く掘り下げました。このチュートリアルの完全なソースコードは、ここにGitHubで見つけることができます。

関連記事: