💆

[TIP] inversifyを使用してDIを本格化する(前回の続き)

に公開

最初に

前回の記事はこちらです。

https://zenn.dev/ncdc/articles/0be30eb5e3f2a0

前回の記事では、簡易的にコンストラクタで部品を渡すだけの DI を体験しました。
今回は、DI ライブラリ inversify を使って、より本格的な依存性注入を実装してみます。

inversify は TypeScript 向けの DI ライブラリで、

  • Decorator で依存関係を明示
  • Container で管理
  • 複数の実装を簡単に切り替え可能

といった特徴があります。

では始めます。

インストール

npm install inversify reflect-metadata

reflect-metadatainversify が型情報を取得するために必要です。

Typescripではtsconfig.jsonに以下の設定が必要です。

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

基本の使い方

  • @injectable():DI コンテナで管理可能なクラスに付与
  • @inject(TYPES.xxx):依存するクラスを注入
  • Container:依存関係の登録・解決

車サービスを inversify で書き換える

TYPESを定義します。

// src/infrastructure/types.ts
export const TYPES = {
  EngineRepository: Symbol.for("EngineRepository"),
  CarService: Symbol.for("CarService"),
};

Repository に @injectable を付与します。

// src/repository/engineRepo.ts
import { injectable } from "inversify";
import { EngineType } from '../domain/car';

@injectable()
export class EngineRepository {
  private engines: EngineType[] = ['V6', 'EV', 'V8'];

  findEngine(carId: string): EngineType {
    return this.engines[Number(carId) % this.engines.length];
  }
}

Service に @inject を付与します。

// src/service/carService.ts
import { injectable, inject } from "inversify";
import { EngineRepository } from "../repository/engineRepo";
import { Car, EngineType } from "../domain/car";
import { TYPES } from "../infrastructure/types";

@injectable()
export class CarService {
  private cars: Car[] = [];

  constructor(@inject(TYPES.EngineRepository) private engineRepo: EngineRepository) {}

  createCar(id: string, name: string): Car {
    const engine: EngineType = this.engineRepo.findEngine(id);
    const car = new Car(id, name, engine);
    this.cars.push(car);
    return car;
  }

  getCar(id: string): Car | undefined {
    return this.cars.find(c => c.id === id);
  }

  driveCar(id: string, distance: number) {
    const car = this.getCar(id);
    if (!car) throw new Error('Car not found');
    car.start();
    car.drive(distance);
    car.stop();
    return car.getMileage();
  }
}
  • TYPES.EngineRepository でコンテナに登録されている Repository を指定
  • これにより、CarService を container.get するときに EngineRepository が自動で注入される

Container を作成して依存関係を登録します。

// src/infrastructure/container.ts
import "reflect-metadata";
import { Container } from "inversify";
import { EngineRepository } from "../repository/engineRepo";
import { CarService } from "../service/carService";
import { TYPES } from "./types";

const container = new Container();

container.bind<EngineRepository>(TYPES.EngineRepository).to(EngineRepository).inSingletonScope();
container.bind<CarService>(TYPES.CarService).to(CarService).inSingletonScope();

export { container };
  • bindで登録してます。
  • .inSingletonScope()を付けると、コンテナがインスタンスを1つだけ作るため、同じクラスを呼び出しても同等として扱われるようになります。
    • 以下に例を示します。
// つけなかった場合
container.bind<CarService>(TYPES.CarService).to(CarService);

const carService1 = container.get<CarService>(TYPES.CarService);
const carService2 = container.get<CarService>(TYPES.CarService);

console.log(carService1 === carService2); // false
// つけた場合
container.bind<CarService>(TYPES.CarService)
         .to(CarService)
         .inSingletonScope();

const carService1 = container.get<CarService>(TYPES.CarService);
const carService2 = container.get<CarService>(TYPES.CarService);

console.log(carService1 === carService2); // true

こんな感じです。

Controller内で依存関係注入済みのserviceを取得します。

// src/app.ts
import { Hono } from "hono";
import { container } from './infrastructure/container';
import { TYPES } from './infrastructure/types';
import { CarService } from './service/carService';

const app = new Hono();
const carService = container.get<CarService>(TYPES.CarService);

app.post('/car/:id/:name', (c) => {
  const { id, name } = c.req.param();
  const car = carService.createCar(id, name);
  return c.json({ id: car.id, name: car.name, engine: car.engineType });
});

app.post('/car/:id/drive/:distance', (c) => {
  const { id, distance } = c.req.param();
  try {
    const mileage = carService.driveCar(id, Number(distance));
    return c.json({ id, distance: Number(distance), totalMileage: mileage });
  } catch (err) {
    return c.text((err as Error).message, 400);
  }
});

export default app;

この実装でDIは完了です。
これでテストがしやすく、疎結合でメンテのしやすいコードへ大きく前進できます。

まとめ

今回は、inversifyを利用したDIの実装に焦点を当ててみました。

ライブラリを使用すればさほど難しくなくても、もしこれがライブラリなしで自力実装するとなると、管理も煩雑になり、コード自体も読みにくく、保守性の観点で影響を与えそうだなと個人的には感じました。(ライブラリに感謝)

今回の記事が誰かのお役に立てれば幸いです。

NCDC テックブログ

Discussion