💆
[TIP] inversifyを使用してDIを本格化する(前回の続き)
最初に
前回の記事はこちらです。
前回の記事では、簡易的にコンストラクタで部品を渡すだけの DI を体験しました。
今回は、DI ライブラリ inversify を使って、より本格的な依存性注入を実装してみます。
inversify は TypeScript 向けの DI ライブラリで、
- Decorator で依存関係を明示
- Container で管理
- 複数の実装を簡単に切り替え可能
といった特徴があります。
では始めます。
インストール
npm install inversify reflect-metadata
※ reflect-metadata は inversify が型情報を取得するために必要です。
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株式会社( ncdc.co.jp/ )のテックブログです。 主にエンジニアチームのメンバーが投稿します。 募集中のエンジニアのポジションや、採用している技術スタックの紹介などはこちら( github.com/ncdcdev/recruitment )をご覧ください!
Discussion