LoginSignup
163

More than 3 years have passed since last update.

DIコンテナの実装を理解して、軽量 DI コンテナを自作しよう

Last updated at Posted at 2019-12-03

なぜ DI コンテナを自作するのか

関心の分離がされているアプリケーションは変更に強く、良い設計と言えます。Dependency Injection(以下 DI) は関心の分離を実現する テクニックの 1 つとしてよく見られるパターンです。しかしクラス間の依存関係が増えれば増えるほど、注入する依存を作ることは困難になり、DI のコストは段々と膨らみます。そのようなとき、 依存を自動で解決し、欲しいインスタンスをすぐにとりだせる DI コンテナ は有効な解決手段となり得ます。

JavaScript/TypeScript においても DI コンテナを提供するライブラリが存在します。例えば、InversifyJStsyringe などが知られています。しかし既存の DI コンテナは、DI 以外の機能を持ち、また使い方も多岐にわたるため、知識の習得コストがかかります。そこで 必要最小限の機能しか持たないシンプルで軽量な DI コンテナを自作できないかと考え、実装しました。この記事ではそれを実装したときに学んだことやテクニックを紹介します。

DI 自体の説明は別の資料に纏めてありますので、不安がある方はご覧ください。

自作 DI コンテナに付ける機能

DI コンテナが備えるべき機能はどのようなものでしょうか。私は少なくとも次の機能はサポートされていて欲しいと思いました。

  • decorator ベースでの依存登録がサポートされる
  • interface に依存している場合もサポートされる
  • 1 つのクラスに inject される依存が複数ある場合もサポートされる

decorator ベース

TypeScript に限らず DI コンテナは、依存登録・依存解決の方法を設定ファイルに記述していました。しかし設定を書くことは手間だったり、実装と設定ファイルを見比べる作業が発生したりして、好まれる手順ではありませんでした。そこで依存と注入対象のコードになんらかのマーキングをして、それだけで自動的に依存関係の登録ができるような仕組みが考案されました。その実現方法として decorator が使われます。この依存登録方法は InversifyJS や tsyringe といった既存ライブラリや、Java の Spring 等でも採用・推奨されている方法です。

interface に対応

interface は TypeScript に組み込まれている標準の機能です。皆さんも interface を使って、このような型を書いた経験があると思います。

interface IProps {
  isLoading: boolean;
  data: IUser;
  error: string;
}

interface は上のように型を定義できる機能ですが、クラスを抽象化したものを表現するためにも利用できます。例えば、DB もしくは API に保存する処理持つクラスは interface を使って次のように抽象化することができます。

// DBにアクセスしようがAPIにアクセスしようがそのどちらにも対応できる
interface IRepository {
  getAll: () => IUser[];
}

class DBRepository implements IRepository {
  getAll() {
    // DBにアクセス
  }
}

class APIRepository implements IRepository {
  getAll() {
    // APIにアクセス
  }
}

このとき Repository を使いたいクラス(例えば Domain Service など)は DBRepository や APIRepository に依存するのではなく、IRepository に依存するように作り、Repository の利用側は使う IRepository に従ったクラスを実装すれば、依存を自由に入れ替えることができます。

class UserService {
  private readonly repository: IRepository;

  constrcutor(repo: IRepository) {
    this.repository = repo;
  }

  getAllUser() {
    return this.repository.getAll();
  }
}

// DBRepository も APIRepository も同じ IRepository の実装なので、両方とも UserService に injection できる
const serviceA = new UserService(new DBRepository());
const serviceB = new UserService(new APIRepository());

しかし TypeScript においては interface はただ型検査時に使われるものでしかなく、トランスパイルすると消えます。(消える例)そのため TypeScript 製の DI コンテナは、何もしなければ interface に紐づいた詳細を見つけて DI することができません。

簡単には満たせない要求

このように DI コンテナを作ろうとすると、decorator の実装と interface への対応という 2 つの課題にぶつかります。そこで InversifyJS や tsyringe を実装した先人たちはどのようにして解決したのか、実装を読んで学んでみましょう。

DI コンテナライブラリを読み進めるために必要な知識

実装を読みましょうと言ったものの、その前に必要な知識を整理しましょう1。まず、DI コンテナは decorator によって依存が登録され、Container クラスに解決したい対象を渡すことで依存を解決、インスタンスを取得できる仕組みです。依存を登録するときには decorator と Reflection Metadata API というものを利用するため、それらについて復習しましょう。

decorator

TypeScript の decorator は、class declaration, method, accessor, property を修飾できる機能です。ここでいう修飾の定義は難しいですが、修飾されたものは、decorator 関数の中から操作することができるということだけ覚えておいてください。例えば関数の結果を URI 変換して書き換える decorator は次のように書けます。( https://qiita.com/taqm/items/4bfd26dfa1f9610128bc から例を拝借しています)

// decorator
function uriEncoded(target: any, propKey: string, desc: PropertyDescriptor) {
  const method = desc.value;
  desc.value = function() {
    const res = Reflect.apply(method, this, arguments);
    if (typeof res === "string") {
      return encodeURIComponent(res);
    }
    return res;
  };
}

class Sample {
  @uriEncoded
  hoge(): string {
    return "こんにちは";
  }
}

console.log(new Sample().hoge());
// 出力
// %E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF

このように decorator は実行時にメソッドの実行などに割り込んで処理を挟み込むことができます。さらに decorator がとる引数は修飾対象が含まれているので、実行時に挙動を変えることが可能になります。

余談ですが decorator はそのまま定義するのではなく、decorator を返す関数などを用意して使われます。その場での設定を埋め込んだ decorator を作りたいことがあるからです。そのような関数は decorator factory と呼ばれます。

DI コンテナでは実行後にクラス decorator 経由でコンストラクタを参照し、そのコンストラクタが必要としている依存を取り出し、DI コンテナに保存します。

Reflection Metadata API

class declaration decorator を利用することで、クラスのコンストラクタにアクセスすることはできるようになります。しかし、これではまだコンストラクタが必要としている依存を取り出すことができません。

例えば下のコードの console.log で出力されるものは class そのものです。

function classDecorator<T extends { new (...args: any[]): {} }>(
  constructor: T
) {
  console.log(constructor);
  return class extends constructor {};
}

@classDecorator
class Hoge {
  constructor(hoge: string) {}
}

いま欲しいのは constructor に注入されたhogeだけです。これを抽出するためには Reflection という機能を使う必要があります。

Reflection

Reflect は JavaScript の機能です。公式の説明をそのまま引用すると「Reflect は、インターセプトが可能な JavaScript 操作に対するメソッドを提供するビルトインオブジェクト」です。この機能を使うことで、コンストラクタ の取得や実行ができます。

function classDecorator<T extends { new (...args: any[]): {} }>(
  constructor: T
) {
  console.log(Reflect.get(Hoge, "constructor"));
  console.log(Reflect.construct(Hoge, []));
  return class extends constructor {};
}

@classDecorator
class Hoge {
  hoge: string;
  constructor(hoge: string) {
    console.log("hey");
  }
}

しかし、これでも constructor の引数の情報を引っ張ってくることはできません。そこでこの Reflect を拡張します。

reflect-metadata

reflect-metadata というライブラリを入れることで Metadata Reflection API が使えるようになります。これは TypeScript 開発チームの一部が開発に参加しているライブラリです。公式によると、次のような背景と目的を持って生まれました。(一部省略)

background

  • Decorators add the ability to augment a class and its members as the class is defined, through a declarative syntax.
  • Languages like C# (.NET), and Java support attributes or annotations that add metadata to types, along with a reflective API for reading metadata.

goals

  • A number of use cases (Composition/Dependency Injection, Runtime Type Assertions, Reflection/Mirroring, Testing) want the ability to add additional metadata to a class in a consistent manner.
  • A consistent approach is needed for various tools and libraries to be able to reason over metadata.

goals にある通り DI をサポートする機能がこの拡張で手に入ります。具体的には consturcotr からの引数取得と、interface 経由で injection するための一時的に interface と実装との紐付けの保管です。

Metadata Reflection API で何ができるようになるかは、Detailed proposal をご参照ください。その中で、DI コンテナを作るために必要になる機能は次の 3 つのみです。

getMetadata

Reflect.getMetadata(designKey, target) を呼び出すことができます。これは、target(class や function)が持つ情報を取得することができます。どのような情報を取得できるかは designKey で指定でき、それぞれ次のような情報が取得できます。

key 名 取得できる情報
"design:type" 引数の型
"design:paramtypes" 引数の型の配列
"design:returntype" 戻り値の型

どのような情報が帰ってくるかの詳しい情報は Decorators & metadata reflection in TypeScript: From Novice to Expert (Part IV)にまとまっているのでご参照ください。

defineMetadata

Reflect.defineMetadata(metadataKey, metadataValue, target)を実行します。これにより、decorator の修飾対象に、ある key に対するメタデータの組を保存できます。そしてこれは後述する getOwnMetadata を利用することでそのメタデータを取り出すことができます。自作 DI コンテナの文脈でいうと、依存している interface の具象クラスを、その interface に紐づけるために使います。

getOwnMetadata

Reflect.getOwnMetadata(metadataKey, target) によって defineMetadata で登録したメタデータを取り出すことができます。これは自作 DI コンテナの文脈でいうと、依存している interface の具象クラスがあるかどうかを調べるために使います。

既存の実装を読んでみよう 〜tsyringe を例に〜

tsyringe は Microsoft が公開した DI コンテナです。@injectable で依存を登録し、 @inject で interface への依存を注入できます。コンテナに登録された依存関係からインスタンスを取り出すためには container.resolve(${class name}) を実行します。基本的にはこの 3 つだけ覚えておけばよく設定ファイルも不要なため2、手軽です。実際のところ、これ以外の機能はほぼないため学習コストも低くシンプルで、DI コンテナを 3 つほど試したことがある私からしても一番好みです。まずはこの tsyringe を読むことで、DI コンテナはどのように実装されているかをみていきましょう。

クラス間の依存を登録(injectable)

injectable.tsにこの injectable decorator factory があります。

function injectable<T>(): (target: constructor<T>) => void {
  return function(target: constructor<T>): void {
    typeInfo.set(target, getParamInfo(target));
  };
}

ここでは なんらかの store に constructor を key にした、parameterInfo を保存していることが分かります。
この parameterInfo を作成している getParamInfo の実装をみてみましょう。

export function getParamInfo(target: constructor<any>): any[] {
  const params: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
  const injectionTokens: Dictionary<InjectionToken<any>> =
    Reflect.getOwnMetadata(INJECTION_TOKEN_METADATA_KEY, target) || {};
  Object.keys(injectionTokens).forEach(key => {
    params[+key] = injectionTokens[key];
  });

  return params;
}

Reflect.getMetadata("design:paramtypes", target) を使えば、constructor に紐づいている変数名や型情報をオブジェクトとして取得できます。ここでは constructor が要求している依存の情報を取得し、返却しています。

間に挟まっているコードは、interface に依存している場合に使う機能です。詳しくは次の節で紹介します。

interface への依存を登録(inject)

inject.tsにこの decorator factory があります。

function inject(
  token: InjectionToken<any>
): (target: any, propertyKey: string | symbol, parameterIndex: number) => any {
  return defineInjectionTokenMetadata(token);
}

export function defineInjectionTokenMetadata(
  data: any
): (target: any, propertyKey: string | symbol, parameterIndex: number) => any {
  return function(
    target: any,
    _propertyKey: string | symbol,
    parameterIndex: number
  ): any {
    const injectionTokens =
      Reflect.getOwnMetadata(INJECTION_TOKEN_METADATA_KEY, target) || {};
    injectionTokens[parameterIndex] = data;
    Reflect.defineMetadata(
      INJECTION_TOKEN_METADATA_KEY,
      injectionTokens,
      target
    );
  };
}

inject が必要になる背景

先ほどの injectable が interface を実装したクラスである場合、実際にコンストラクタに注入された依存の具象は何かはわかりません。最初の Repository の例で言えば、Repository を使う Domain Service はどの永続化レイヤーに依存するかは知りません。

// DBにアクセスしようがAPIにアクセスしようがそのどちらにも対応できる
interface IRepository {
  getAll: () => IUser[];
}

class DBRepository implements IRepository {
  getAll() {
    // DBにアクセス
  }
}

class APIRepository implements IRepository {
  getAll() {
    // APIにアクセス
  }
}

@injectable()
class UserService {
  private readonly repository: IRepository;

  constrcutor(repo: IRepository) {
    this.repository = repo;
  }

  getAllUser() {
    return this.repository.getAll();
  }
}

const serviceA = new UserService(new DBRepository());
const serviceA = new UserService(new APIRepository());

UserService に @injectable() decorator があることに注意してください。このとき Service についた @injectable() の中で Reflection Metadata API を用いて constructor の情報をみたとき、serviceA, serviceB において双方とも IRepository という情報しか手に入りません。そのため実際に注入された依存は何かを明示的に伝える必要があります。tsyringe ではその機能を @inject() として提供しており、constructor の引数で利用します。

@injectable()
class UserService {
  private readonly repository: IRepository;

  constrcutor(@inject("IDBRepository") repo: IRepository) {
    this.repository = repo;
  }

  getAllUser() {
    return this.repository.getAll();
  }
}

このとき @inject() の引数は、他の inject の引数と衝突しなければなんでもいいですが、依存の名前などにしておくとよいでしょう。衝突を避けるために Enum を定義したり、Symbol を利用することもあります。

inject はなにをしてくれているのか

injectable に次のコードがあったことを思い出してください。

const injectionTokens: Dictionary<InjectionToken<any>> =
  Reflect.getOwnMetadata(INJECTION_TOKEN_METADATA_KEY, target) || {};
Object.keys(injectionTokens).forEach(key => {
  params[+key] = injectionTokens[key];
});
const injectionTokens: Dictionary<InjectionToken<any>> =
  Reflect.getOwnMetadata(INJECTION_TOKEN_METADATA_KEY, target) || {};
Object.keys(injectionTokens).forEach(key => {
  params[+key] = injectionTokens[key];
});

const injectionTokens: Dictionary<InjectionToken<any>> = Reflect.getOwnMetadata(INJECTION_TOKEN_METADATA_KEY, target) || {};ここでは登録したい依存に紐づく何かがないかを探しています。Reflect Metadata API では修飾対象の情報を取得するだけでなく、metadata を保存する Map(?)的なものも提供しています。

ここでは仮に依存が interface だった場合にその実装が何かを探しに行っています。

Reflect.defineMetadata(INJECTION_TOKEN_METADATA_KEY, injectionTokens, target);

つまり、この機能を呼ぶことで、interface 越しにも依存が解決できるようになるわけです。

依存を解決(resolve)

一番読み応えのある機能でした。

コンテナ

DI コンテナはコンテナとよばれる物の中に依存を登録し、そこから依存を解決していきインスタンスを生成してくれます。そのコンテナ自体は class として定義されています。

class InternalDependencyContainer implements DependencyContainer {
  // 300行くらい続く
}

依存の解決

@injectable で登録した Map は {constructor: [dependency, ...], ...} といった組を持っています。container に生えている resolve(arg)メソッドは渡された引数の constructor を Map にある依存関係を参照しながらインスタンス化していきます。

Map をみると key にある constructor をインスタンス化するためには dependency を引数に入れてインスタンス化する必要があります。ただし、dependency をインスタンス化するためには、その constructor で再度 Map を検索し、インスタンス化可能かどうかを確認する必要があります。そのためこの resolve メソッドは、依存解決を再帰的に実行します。

public resolve<T>(
    token: InjectionToken<T>,
    context: ResolutionContext = new ResolutionContext()
  ): T {
    const registration = this.getRegistration(token);

    // - 中略 -

    return this.construct(token as constructor<T>, context);
  }

  private construct<T>(ctor: constructor<T>, context: ResolutionContext): T {
     // - 中略 -

    const paramInfo = typeInfo.get(ctor);

    // - 中略 -

    const params = paramInfo.map(param => {
        // - 中略
      return this.resolve(param, context);
    });

    return new ctor(...params);
  }

ここで注意したいことは context と呼ばれるものです。これは ResolutionContext という名前の型が付いており、その名の通り依存解決の途中結果を保存するための Map です。これを掘っていくと {[Provider]: any} という組の Map であることがわかりますが、ほとんどのユースケースでは{[constructor]: any}という組になるでしょう。

これは何を表しているかといえば、さらに読み進めていくと、依存解決時に対象の constructor をインスタンス化したときの組を保存していることがわかります。

private resolveRegistration<T>(
    registration: Registration,
    context: ResolutionContext
  ): T {

    // - 中略 -

    if (registration.options.lifecycle === Lifecycle.ResolutionScoped) {
      context.scopedResolutions.set(registration, resolved);
    }

    return resolved;
  }

自作軽量 DI コンテナに挑戦しよう

ここまでで tsyringe で DI するときに何がされているかを読みすすめました。コードをみて気づかれたかもしれませんが、かなり中略しており、実際にはさまざまな処理がたくさんあります。また tsyringe には class constuctor 以外の依存の解決、複数の container の利用、双方向の依存に対処するなどといったユースケースも想定されており、ただ DI をしたいというニーズに対しては機能過多な部分があります。機能過多だと、ただ使いたいだけというニーズによっては学習コストがかかり障壁ともなり得り、ソースコードリーディングの際にも読みにくいポイントが生まれたりもします。そこで decorator ベースで DI をする最小構成を作ってみましょう。

依存を保存できるコンテナを作る

まず依存を登録できるコンテナを作ります。
複数コンテナでの運用は考えないので、シングルトンで作ります。

class Container {
  private static instance: Container;

  data: Map<constructor<any>, constructor<any>[]>;
  context: Map<constructor<any>, constructor<any>>;

  private constructor() {
    this.data = new Map<constructor<any>, constructor<any>[]>();
    this.context = new Map<any, constructor<any>>();
  }

  static getInstance() {
    if (!Container.instance) {
      Container.instance = new Container();
    }
    return Container.instance;
  }
}

export default Container;

constructor という型は別の場所で、次のように定義します。

export type constructor<T> = {
  new (...args: any[]): T;
};

そして DI コンテナは依存を登録できるので、登録するための関数をコンテナに生やします。

class Container {
  // -中略-

  public register(constructor: constructor<any>, depends: constructor<any>[]) {
    this.data.set(constructor, depends);
  }
}

依存を登録する機能を作る

interface を経由しない場合

次に依存を登録する機能を作ります。
これは tsyringe に倣って @injectable() という名の Class Decorator を定義しましょう。

export const injectable = (): ClassDecorator => {
  return target => {
    const params: any[] =
      Reflect.getMetadata("design:paramtypes", target) || [];
    Container.getInstance().register(target, params);
  };
};

Reflect.getMetadata("design:paramtypes", target)で decorator で修飾された class の constrcutor の引数の constructor を取得します。そしてそれを、Container.getInstance().register(target, params); で DI コンテナに保存します。

interface を経由させる場合

これも tsyringe に倣って @inject(${class名}) という Class Decorator を定義しましょう。

この decorator は DI コンテナに登録される依存を上書きするものです。ユースケースとしては@injectable()経由で登録された依存情報が interface のものだったときです。これを@inject(${class名})で具象クラスの constructor にすり替えるようにしたいです。

まず、@inject()は次のように定義します。

export const inject = (
  token: InjectionToken<any>
): ((
  target: any,
  propertyKey: string | symbol,
  parameterIndex: number
) => any) => {
  return defineInjectionTokenMetadata(token);
};

const defineInjectionTokenMetadata = (data: any): ((target: any) => any) => {
  return function(target: any): any {
    const interfaceName: any = {};
    interfaceName[0] = data;
    Reflect.defineMetadata(INJECTION_TOKEN_METADATA_KEY, interfaceName, target);
  };
};

この実装は tsyringe とほとんど同じものです。
@inject()の定義に propertyKey,parameterIndex と言ったものが出てきますが、これは使いません。
しかし@inject()は decorator factory なので decorator が取りうる引数をとる関数を返さないといけません。そのためこの不要な引数は省略することができません。

decorator factory である@inject()が返す decorator では、次のことがされています。

Reflect.defineMetadata(INJECTION_TOKEN_METADATA_KEY, interfaceName, target);

これによりINJECTION_TOKEN_METADATA_KEYというキーで interfaceName と target が紐づいていることを引っ張ってこれるようになりました。

そしてすり替えてコンテナに保存できるように @injectable を拡張します。

export const injectable = (): ClassDecorator => {
  return target => {
    const params: any[] =
      Reflect.getMetadata("design:paramtypes", target) || [];

    // NEW
    const injectionTokens: Dictionary<InjectionToken<any>> =
      Reflect.getOwnMetadata(INJECTION_TOKEN_METADATA_KEY, target) || {};
    Object.keys(injectionTokens).forEach(key => {
      params[+key] = injectionTokens[key];
    });
    // NEW

    Container.getInstance().register(target, params);
  };
};

これで interface 越しに依存を登録できるようになりました。

依存を解決する機能を作る

それでは登録した依存を解決し、インスタンスを取り出す機能を作りましょう。

依存を解決する関数を作る

依存を解決する関数として resolveを作りましょう。

public resolve(ctor: constructor<any>) {
  // 受け取った依存を注入するために依存をインスタンス化する関数を呼び出す(できないときもある)
  this.resolveInstance(ctor);

  // resolveしたいクラスの依存を取得する
  const dependantClasses = this.data.get(ctor);

  // その依存のインスタンスを全て取得する(この時点で全依存はインスタンスされている想定)
  if (!dependantClasses) return;
  const instances = dependantClasses.map(cls => this.context.get(cls));

  // 依存を全て注入してインスタンス化
  return new ctor(...instances)
}

public resolve(ctor: constructor<any>) {

  // 注入しなければいけない依存のコンストラクタを取得
  const targetDependencies = this.data.get(ctor);
  if (targetDependencies && targetDependencies.length > 0) {
    // 注入しなければいけない依存がなければ即時インスタンス化
    const instance = new ctor();
    this.context.set(ctor, instance);
  }

  // 注入しなければいけない依存をインスタンス化する
  //(引数が注入される側なのはI/Fとしてはいけてないです。すみません。)
  this.resolveInstance(ctor);
  const dependantClasses = this.data.get(ctor);

  // 必要な解決済み依存を取得
  const instances = dependantClasses.map(cls => this.context.get(cls));

  // 依存を全て注入してインスタンス化
  return new ctor(...instances);
}

依存の解決とは、インスタンス化を指します。
しかし、依存を解決しようとするも、その依存をインスタンス化しようとして、別の依存がある場合もあります。
そのため、依存解決を再帰的に行う仕組みを作ります

private resolveInstance(ctor: any) {
  // 引数のコンストラクタをインスタンス化するために必要な依存を取得
  const depends = this.data.get(ctor);
  if (!depends) {
    // もし必要な依存がないなら、そのままコンストラクタをインスタンス化する
    const i = new ctor();
    this.context.set(ctor, i);
    return;
  }

  // 必要な依存あるなら、そのままinstance化できるまで resolveを再帰的に呼ぶ
  // 依存が一方向であることを前提にしているのでこう書いても最終的に依存を解決できる
  // (単独でインスタンス化できるコンストラクタにいつか出会えるから)
  this.resolve(depends[0]);

  // 必要な依存の全インスタンスを取得
  const dependInstances = depends.map(d => {
    return this.context.get(d);
  });

  // その依存を注入し保存する
  const instance = new ctor(...dependInstances);
  this.context.set(ctor, instance);
}

思ったよりも resolve をシュッと書けたのではないでしょうか。実は双方向の依存をサポートする機能を入れていないので、このように単純にすることができました。クリーンアーキテクチャの本などでは依存の方向を一方向にするように書かれており、自分はそのような設計しかしないのでサポートをしませんでした。そのおかげで DI コンテナの設計をかなり削ることができました。

自作 DI コンテナはちゃんと動くのか

こちら が自作したDIコンテナです。ここにある example を実行します。

依存の階層が多い場合

// so many nest example

import { injectable } from "../main/Injectable";
import "reflect-metadata";
import Container from "../main/Container";

class A {
  call() {
    console.log("CALL A");
  }
}

@injectable()
class B {
  a: A;

  constructor(a: A) {
    this.a = a;
  }
}

@injectable()
class C {
  b: B;

  constructor(b: B) {
    this.b = b;
  }
}

@injectable()
class D {
  c: C;

  constructor(c: C) {
    this.c = c;
  }
}

@injectable()
class E {
  d: D;

  constructor(d: D) {
    this.d = d;
  }
}

const container = Container.getInstance();
const e = container.resolve(E);
e.d.c.b.a.call();

これを実行すると・・・

$ node dist/example/test1.js
CALL A

インターフェースに依存する場合

// can revolve via interface

import Container from "../main/Container";
import { injectable } from "../main/Injectable";
import "reflect-metadata";
import { inject } from "../main/inject";

interface IRepository {
  read: () => number[];
  create: (val: number) => void;
}

interface IStoreAdapter {
  read: () => number[]; // in real, should return a DTO.
  create: (val: number) => void;
}

class MemoryStoreImpl implements IStoreAdapter {
  private store: number[] = [];
  read() {
    return this.store;
  }

  create(val: number) {
    this.store.push(val);
  }
}

@injectable()
class DBRepositoryImpl implements IRepository {
  adapter: IStoreAdapter;

  constructor(@inject(MemoryStoreImpl) adapter: IStoreAdapter) {
    this.adapter = adapter;
  }

  read() {
    return this.adapter.read();
  }

  create(val: number) {
    this.adapter.create(val);
  }
}

@injectable()
class APIRepositoryImpl implements IRepository {
  read() {
    console.log("get data");
    return [1, 2, 3];
  }

  create(val: number) {
    console.log("post data");
  }
}

@injectable()
class Service {
  repo: IRepository;

  constructor(@inject(DBRepositoryImpl) repo: IRepository) {
    this.repo = repo;
  }

  public find() {
    return this.repo.read();
  }

  public save(val: number) {
    this.repo.create(val);
  }
}

// interface test

const container = Container.getInstance();
const service = container.resolve(Service);
service.save(1);
service.save(2);
const data = service.find();
console.log("the value: ", data);

これを実行すると・・・

$ node dist/example/test2.js
the value:  [ 1, 2 ]

複数の依存を受け取る場合

import { injectable } from "../main/Injectable";
import "reflect-metadata";
import Container from "../main/Container";

class Hoge {
  call() {
    console.log("hogeeeeeeeeeeee");
  }
}

class Piyo {
  call() {
    console.log("piyooooooooooooo");
  }
}

@injectable()
class Fuga {
  hoge: Hoge;
  piyo: Piyo;

  constructor(hoge: Hoge, piyo: Piyo) {
    this.hoge = hoge;
    this.piyo = piyo;
  }
}

@injectable()
class Foo {
  fuga: Fuga;

  constructor(fuga: Fuga) {
    this.fuga = fuga;
  }
}

const container = Container.getInstance();
const foo = container.resolve(Foo);
foo.fuga.hoge.call();
foo.fuga.piyo.call();
$ node dist/example/test2.js
hogeeeeeeeeeeee
piyooooooooooooo

まとめ

いかがでしたか。自作 DI コンテナは仕組みさえわかれば以外と簡単に作れます。
それに tsyringe に比べるとかなりシンプルにすることができました。
しかし実際に運用されるコードや規模の大きいコードを書こうとすると、「テストを書きやすくするために、設定ファイルによってあとから依存を差し替えたい」「いちいちコンテナを作って resolve せずに、自動で解決したものをインスタンス化して取り出したい」といったニーズが出てくるでしょう。
残念ながら自作 DI コンテナにはその機能はないですし、恐らくそのような機能を生やしていくと tsyringe に近づいていていくと思います。
DI コンテナはあまり多様性がなく、色々な機能が足されていっているものだと思います。
その中でも tsyringe は必要最小限の機能を全て盛り込んだ最小の DI コンテナだと思っており、お勧めします。


  1. 今はデコレータベースのものを読むこととし、PROXY ベースのものは扱いません。 

  2. test のときだけ依存をモックに変えるようなことをしようとすると設定ファイルが出てくる 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
163