💆

[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を実践的に使用する方法を記事にしようと思います。

次回記事はこちらです。
https://zenn.dev/ncdc/articles/128a3cbef61bd0

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

NCDC テックブログ

Discussion