ソースコードで理解するNestJS④: DependenciesScanner, NestContainer

# 前回

https://qiita.com/fj-th/items/2b6ff55f62ee5d0fdf3c

前回までで、NestJSのルーティングがどのように実現されているのかを見ました。`@Controller`や`@Get`といったデコレータによって付与されたメタデータをもとに、`RoutesResolver`や`RouterExplorer`の働きによってルートパスとハンドラがHTTPサーバーに登録されます。

今回はNestJSのもう一つの重要トピックである依存性注入 (DI)を見ていきます。以前の記事で見た通り、鍵となるのは`DependenciesScanner#insertModule`です。

# `@Injectable`のおさらい
NestJSにおけるDIでは`@Injectable`デコレータが重要です。以前の記事で見た通り、このデコレータは以下のように`Reflect.defineMetadata`で修飾先のクラスのコンストラクタ関数にメタデータを付与する役割を持っています。

```typescript
export function Injectable(options?: InjectableOptions): ClassDecorator {
  return (target: object) => {
    Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target);
    Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target);
  };
}
```
二つ目の`SCOPE_OPTIONS_METADATA`は`@Injectable`で指定するinjection scopeを表します。

https://docs.nestjs.com/fundamentals/injection-scopes

DIを追う上ではあまり重要ではないので無視して、`INJECTABLE_WATERMARK`を追っていくことにします。リポジトリを検索すると、`DependenciesScanner`クラスで定義された`isInjectable`メソッドに行き当たります。このメソッドはモジュールクラスのクラスコンストラクタを受け取り、それが`@Injectable`修飾されているかを返します。

`isInjectable`の利用元である`insertModule`は以前の記事でも登場しました。

```typescript
  public async insertModule(
    moduleDefinition: any,
    scope: Type<unknown>[],
  ): Promise<
    | {
        moduleRef: Module;
        inserted: boolean;
      }
    | undefined
  > {
    const moduleToAdd = this.isForwardReference(moduleDefinition)
      ? moduleDefinition.forwardRef()
      : moduleDefinition;

    if (
      this.isInjectable(moduleToAdd) ||
      this.isController(moduleToAdd) ||
      this.isExceptionFilter(moduleToAdd)
    ) {
      throw new InvalidClassModuleException(moduleDefinition, scope);
    }

    return this.container.addModule(moduleToAdd, scope);
  }

```
`insertModule`は、モジュールのクラスコンストラクタである`moduleDefinition`をDIコンテナである`NestConteiner`のインスタンスである`this.container`に登録し、モジュールクラスをインスタンス化して返します。

`insertModule`がどのように使われているかを通じて`DependenciesScanner`の全体像を理解しましょう。


# DependenciesScanner

https://github.com/nestjs/nest/blob/e4b07eb06b406183e708269e0439f442a1dadb1c/packages/core/scanner.ts

`DependenciesScanner`は、Nestアプリケーションの依存関係を解決し、DIコンテナにそれを登録する役割を果たします。`NestFactory`クラスの`initialze`で利用され、アプリケーションのエントリーポイントになるモジュール(AppModule)を起点にモジュールを再起的にスキャンします。

```typescript

  private async initialize(
    module: any, // エントリーポイントになるAppModuleのコンストラクタ
    container: NestContainer,
    graphInspector: GraphInspector,
    config = new ApplicationConfig(),
    options: NestApplicationContextOptions = {},
    httpServer: HttpServer = null,
  ) {
        // 省略
    
          await dependenciesScanner.scan(module);
          await instanceLoader.createInstancesOfDependencies();
          dependenciesScanner.applyApplicationProviders();

        //省略

```
ここで、`scan`がメインの処理であるAppModuleからの再起的な依存解決を担当し、`applyApplicationProviders`はグローバルに扱われるプロバイダーのうちEnhancer(Intercepter, Pipe, Guard, Filter)であるものの登録を担当しています。`applyApplicationProviders`はひとまず置いておいて、`scan`を見ていきましょう。

## dependenciesScanner.scan(module)

実装を見て見ましょう。各処理の責務をコメントで補足しています。

```typescript

public async scan(
    module: Type<any>, // AppModuleのクラスコンストラクタ
    options?: { overrides?: ModuleOverride[] },
  ) {
    // InternalCoreModuleFactory.createで生成されるモジュールに対するscanForModules
    await this.registerCoreModule(options?.overrides);

    // メイン処理 (再起呼び出しされる)
    await this.scanForModules({
      moduleDefinition: module,
      overrides: options?.overrides,
    });

    // Moduleの解決結果をもとに、各モジュールでImport, Provider, Controller, Exportを反映
    await this.scanModulesForDependencies();

    //　ルートモジュールから出発し、Importsを辿ってモジュール間の「遠さ」を計算して登録
    // 初期化順序や依存関係解決の最適化に利用
    this.calculateModulesDistance();

    // グローバルなEnhancerのスコープ情報をコントローラ, プロバイダーのメタデータに追加する
    this.addScopedEnhancersMetadata();

    // 各モジュールにグローバルスコープのプロバイダーをバインドし、利用可能にする
    this.container.bindGlobalScope();
  }
```

## scanForModules
メイン処理である`scanForModules`を見ていきます。理解優先で一部単純化してあります。重要なのは冒頭の`this.insertOrOverrideModule`で、これは先述の`insertModule`を実行し、DIコンテナである`NestContainer`へのクラスの登録とインスタンス化を行います。`this.insertOrOverrideModule`以降は、import辿って再起的にこれを実行するためのもろもろです。引数の`scope`はAppModuleから現在のモジュールに至るまでの参照解決経路で登場する全てのモジュールのクラスコンストラクタを配列化したものになっています。

```typescript
    public async scanForModules({
    moduleDefinition,   // 現在着目しているモジュールのクラスコンストラクタ
    scope = [],
    ctxRegistry = [],   // クラスコンストラクタ配列 (処理済みモジュール)
    overrides = [],
  }: ModulesScanParameters): Promise<Module[]> {

    // モジュールを追加、ないし上書きする
    // Overrideのことを一旦無視すると、`insertModule`を実行しているだけ
    const { moduleRef: moduleInstance, inserted: moduleInserted } =
      (await this.insertOrOverrideModule(moduleDefinition, overrides, scope)) ??
      {};

    // 無駄な処理をスキップするために処理済みのものを管理
    ctxRegistry.push(moduleDefinition);

    // 着目しているモジュールの@Moduleの引数でimport指定されているモジュール群を取得
    const modules = this.reflectMetadata(
          MODULE_METADATA.IMPORTS,
          moduleDefinition as Type<any>,
        );

    // 現在のモジュールより下位にあるモジュールのインスタンス全体
    let registeredModuleRefs = [];

    // importしているモジュールに関してループ
    for (const [index, innerModule] of modules.entries()) {

      // 処理済みモジュールについてはスキップ
      if (ctxRegistry.includes(innerModule)) {
        continue;
      }

      // モジュール探索を再起実行
      const moduleRefs = await this.scanForModules({
        moduleDefinition: innerModule,
        scope: [].concat(scope, moduleDefinition),
        ctxRegistry,
        overrides,
        lazy,
      });
      
      registeredModuleRefs = registeredModuleRefs.concat(moduleRefs);
    }
    
    if (!moduleInstance) {
      return registeredModuleRefs;
    }

    // グローバルスコープのプロバイダーをバインド
    if (moduleInserted) {
      this.container.bindGlobalsToImports(moduleInstance);
    }

    // 現在のモジュールと、それが依存するモジュールに関する再起処理結果を結合し、インスタンス配列を返す
    return [moduleInstance].concat(registeredModuleRefs);
  }
```

まとめると、`scan`はAppModuleから出発し、`@Module`の引数で指定されたimport情報のメタデータに基づいて再起的に参照解決を行い、`NestContainer`を更新します。次にこの`NestContainer`を見ていきます。

# NestContainer
NestJS アプリケーション全体のモジュール構成と依存関係解決の中核を担っています。モジュールとプロバイダーの登録や解決、グローバルスコープの管理、動的モジュールの取り扱い、HTTPアダプターの設定などの重要な機能を実装しています。

https://github.com/nestjs/nest/blob/e4b07eb06b406183e708269e0439f442a1dadb1c/packages/core/injector/container.ts

`DependenciesScanner`から実行される`addModule`は以下で定義されます。

```typescript
  public async addModule(
    metatype: ModuleMetatype,
    scope: ModuleScope,
  ){
    // DIコンテナ内でモジュールを一意に識別するトークン (ハッシュ文字列)を生成する。
    // scopeやDynamicModuleの設定によって1つのモジュールクラスに対応した複数のモジュールが存在しうる
    const { type, dynamicMetadata, token } =
      await this.moduleCompiler.compile(metatype);

    // 省略

    return {
      moduleRef: await this.setModule(
        {
          token,
          type,
          dynamicMetadata,
        },
        scope,
      ),
      inserted: true,
    };
  }

```

モジュールの登録処理である`setModule`は以下で、ここでモジュールをインスタンス化し、一意な識別子をキーとして、Mapである`ModulesContainer`にそのインスタンスを追加しています。
```typescript
    
    private async setModule(
        { token, dynamicMetadata, type }: ModuleFactory, 
        scope: ModuleScope,
    ){
        const moduleRef = new Module(type, this);
        moduleRef.token = token;
        moduleRef.initOnPreview = this.shouldInitOnPreview(type);
    
        // ModulesContainerにトークンをキーとしてモジュールインスタンスを登録
        this.modules.set(token, moduleRef);

        // type: モジュールのクラスコンストラクタ
        const updatedScope = [].concat(scope, type);

        // 動的モジュールによって導入された依存関係の処理。
        // 動的モジュールのメタデータの保存と、コンテナへのモジュール追加 (addModule)
        await this.addDynamicMetadata(token, dynamicMetadata, updatedScope);
        
        if (this.isGlobalModule(type, dynamicMetadata)) {
          moduleRef.isGlobal = true;
          this.addGlobalModule(moduleRef);
        }
        
        return moduleRef;
    }
```
コメントで書いた通り、静的モジュールと動的モジュールは異なる取り扱いを受けます。静的モジュールの依存関係は`scanForModules`処理されますが、動的モジュールは上記`addDynamicMetadata`内で別途処理されています。

# まとめ
今回は、NestJSのもう一つの重要な機能である依存性注入 (DI) について解説しました。特に、`DependenciesScanner`クラスがこのプロセスの中核を担っており、`scanForModules`メソッドを通じてAppModuleから再帰的に依存関係をスキャンし、DIコンテナである`NestContainer`を更新します。このスキャンと登録のプロセスにより、NestJSはモジュール間の依存関係を把握することができます。

次回は、`NestContainer#setModule`で各モジュールのインスタンスが生成されたあと、どのように依存関係が解決され、各種プロバイダーのインスタンス化と依存性注入が行われるかを見ることにします。鍵になるのは、`NestFactory`の`initialize`で登場した`InstanceLoader#applyApplicationProviders`です。