[TIP] 改めてDI + DDDを理解する
最初に
今更ながら、DIについて曖昧な理解を断ち切りたいと思いました。
そのために、今回は「hono + DI + DDD」を使用して、備忘録として残しておきます。
では始めます。
DI(依存性注入)とは?
巷では、「依存性注入」という小難しい言葉を使用して説明されることが多いですが、
要するに、「部品同士が直接くっつくのではなく、必要な部品は外から渡してあげる仕組み」のことです。
以下に例を示します。
// 直接作る場合(DIに反した作り)
class Car {
private engine = new Engine(); // 車が自分でエンジンを作る
start() {
this.engine.run();
}
}
- Car自体がどんなエンジンか自分で決めている(Car内でエンジンを作るため)
- 変更やテストがしにくい
- new Car().start()のテスト時に、いちいちengineをインスタンス化する必要がある
- 一つなら良くても、複数インスタンス化するとなると...
// 外から渡す場合(DI)
class Car {
constructor(private engine: Engine) {} // エンジンは外から渡す
start() {
this.engine.run();
}
}
const v6Engine = new V6Engine();
const myCar = new Car(v6Engine); // 外から部品を渡す
- Carは どんなエンジンでも走れる
- テスト用のダミーエンジンも簡単に渡せる
- 変更があってもCar自体を修正する必要がない
つまり、「車はどんなエンジンが入っているか気にせず走れる」ようにするのがDIです。
DDD(ドメイン駆動設計)って何?
DDDは「システムを現実のルールに沿って部品ごとに整理する方法」です。
車で考えると…
- ドメイン(業務ルール):車は走る、止まる
- 部品(RepositoryやService):エンジン、タイヤ、ブレーキ
- サービス(処理):エンジンを動かす、ブレーキをかける
つまり 現実世界の「車の部品と役割」をそのままコードに置き換えるイメージです。
Honoで車サービスを作る
Honoは軽量なWebフレームワークです。
車の例で「車情報API」を作ってみます。
ディレクトリ構成
src/
├─ domain/
│ └─ car.ts
├─ repository/
│ └─ engineRepo.ts
├─ service/
│ └─ carService.ts
└─ app.ts
ドメイン
// src/domain/car.ts
export type EngineType = 'V6' | 'EV' | 'V8';
export class Car {
private mileage: number = 0; // 走行距離
private running: boolean = false; // エンジン状態
constructor(
public id: string,
public name: string,
public engineType: EngineType
) {}
start() {
if (!this.running) {
this.running = true;
console.log(`${this.name}の${this.engineType}エンジンを始動`);
}
}
stop() {
if (this.running) {
this.running = false;
console.log(`${this.name}を停止`);
}
}
drive(distance: number) {
if (!this.running) throw new Error('エンジンがかかっていません');
this.mileage += distance;
console.log(`${this.name}は${distance}km走行しました`);
}
getMileage() {
return this.mileage;
}
isRunning() {
return this.running;
}
}
ドメインは業務ルールなので、各種メソッド内にクラス特有の定められたルールに基づき実装を行います。
リポジトリ(部品倉庫)
// src/repository/engineRepo.ts
import { EngineType } from '../domain/car';
export class EngineRepository {
private engines: EngineType[] = ['V6', 'EV', 'V8'];
findEngine(carId: string): EngineType {
// 車IDでエンジンタイプを簡易決定
return this.engines[Number(carId) % this.engines.length];
}
}
サービス(車の頭脳)
// src/service/carService.ts
import { EngineRepository } from '../repository/engineRepo';
import { Car, EngineType } from '../domain/car';
export class CarService {
private cars: Car[] = [];
constructor(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();
}
}
HonoでAPI接続
// src/app.ts
import { Hono } from 'hono';
import { EngineRepository } from './repository/engineRepo';
import { CarService } from './service/carService';
const app = new Hono();
// 部品を手動で渡す(DIの実現)
const engineRepo = new EngineRepository();
const carService = new CarService(engineRepo);
// 車を作る
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の基本的な考え方です)
まとめ
DI + DDDの中身をある程度理解するには、単純な実装から始めると個人的にはわかりやすいと感じました。
今回の例で言うと以下がわかりました。
- DIの本質:部品を外から渡すことで、サービスやリポジトリを簡単に差し替えられる
- Inversifyなしでも、コンストラクタで渡すだけでDIは実現可能
- DDDでは ドメインに振る舞いを持たせる ことで現実のルールを反映
- HonoはAPI窓口、CarServiceは頭脳、EngineRepositoryは部品倉庫
🚗 車の例まとめ
Car = 車(状態と振る舞い)
EngineRepository = 部品倉庫
CarService = 車の頭脳
DI = 「部品を外から渡す仕組み」
次は「inversify」を利用して更にDIを実践的に使用する方法を記事にしようと思います。
次回記事はこちらです。
今回の記事が誰かのお役に立てれば幸いです。
NCDC株式会社( ncdc.co.jp/ )のテックブログです。 主にエンジニアチームのメンバーが投稿します。 募集中のエンジニアのポジションや、採用している技術スタックの紹介などはこちら( github.com/ncdcdev/recruitment )をご覧ください!
Discussion