はじめに
この記事は「リファクタリングを通して学ぶドメイン駆動設計」シリーズのPart7(最終回)です。
前回はコントローラー(Controller) の実装について説明しました。今回は依存性注入(Dependency Injection) について解説し、これまで作成してきた各層を組み立てて、実際に動作するアプリケーションを完成させます。
開発環境
開発環境は以下の通りです。
- Windows11
- Docker Engine 27.0.3
- Docker Compose 2
- PostgreSQL 18.1
- Node.js 24.11.0
- npm 11.6.2
- TypeScript 5.9.3
- Express 5.1.0
- Prisma 6.18.0
- Zod 4.1.12
依存性注入の設定
依存性注入(Dependency Injection: DI) とは、クラスが必要とする他のクラスを外部から渡す手法です。
エントリーポイントで各層のインスタンスを生成し、依存関係を解決します。
// main.ts
import express from 'express';
import { PrismaClient } from '@prisma/client';
const app = express();
const prisma = new PrismaClient();
const PORT = process.env.PORT || 3000;
app.use(express.json());
// 依存性注入の設定
const userRepository = new PrismaUserRepository(prisma);
const createUserUseCase = new CreateUserUseCase(userRepository);
const getUserUseCase = new GetUserUseCase(userRepository);
const getAllUsersUseCase = new GetAllUsersUseCase(userRepository);
const updateUserUseCase = new UpdateUserUseCase(userRepository);
const deleteUserUseCase = new DeleteUserUseCase(userRepository);
const userController = new UserController(
createUserUseCase,
getUserUseCase,
getAllUsersUseCase,
updateUserUseCase,
deleteUserUseCase
);
// ルーティング
app.get('/', (req, res) => {
res.json({ message: 'Server is running' });
});
app.post('/users', (req, res) => userController.create(req, res));
app.get('/users', (req, res) => userController.getAll(req, res));
app.get('/users/:id', (req, res) => userController.getById(req, res));
app.put('/users/:id', (req, res) => userController.update(req, res));
app.delete('/users/:id', (req, res) => userController.delete(req, res));
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
process.on('SIGINT', async () => {
await prisma.$disconnect();
process.exit(0);
});
依存関係の流れ
依存性注入により、以下のような依存関係が構築されます。
main.ts
├─ PrismaClient (Prisma)
│ └─ PrismaUserRepository (インフラ層)
│ ├─ CreateUserUseCase (アプリケーション層)
│ ├─ GetUserUseCase (アプリケーション層)
│ ├─ GetAllUsersUseCase (アプリケーション層)
│ ├─ UpdateUserUseCase (アプリケーション層)
│ └─ DeleteUserUseCase (アプリケーション層)
│ └─ UserController (プレゼンテーション層)
│ └─ setupUserRoutes (ルーティング)
└─ Express App
依存性注入のポイント
ポイント1: 依存関係の一方向性
依存関係は常に外側から内側へ向かいます。
プレゼンテーション層
↓ 依存
アプリケーション層
↓ 依存
ドメイン層
↓ 依存(インターフェース経由)
インフラ層
重要な原則:
- ドメイン層は他の層に依存しない
- アプリケーション層はドメイン層にのみ依存
- インフラ層はドメイン層のインターフェースを実装
- プレゼンテーション層はアプリケーション層に依存
ポイント2: インスタンス生成の順序
依存性注入では、インスタンスを依存される側から生成します。
// 1. 最も依存されるもの(インフラ層)
const prisma = new PrismaClient();
const userRepository = new PrismaUserRepository(prisma);
// 2. 中間層(アプリケーション層)
const createUserUseCase = new CreateUserUseCase(userRepository);
const getUserUseCase = new GetUserUseCase(userRepository);
// ...
// 3. 最も依存するもの(プレゼンテーション層)
const userController = new UserController(
createUserUseCase,
getUserUseCase,
getAllUsersUseCase,
updateUserUseCase,
deleteUserUseCase
);
ポイント3: テスト時の差し替え
依存性注入により、テスト時に実装を差し替えられます。
// 本番環境
const userRepository = new PrismaUserRepository(prisma);
const useCase = new CreateUserUseCase(userRepository);
// テスト環境
const mockRepository: IUserRepository = {
save: jest.fn(),
findById: jest.fn(),
findAll: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
existsByEmail: jest.fn(),
};
const useCase = new CreateUserUseCase(mockRepository);
ポイント4: 変更への柔軟性
データベースを変更する場合、リポジトリの実装を差し替えるだけです。
// Prismaを使用
const userRepository = new PrismaUserRepository(prisma);
// TypeORMに変更
const userRepository = new TypeORMUserRepository(dataSource);
// ユースケース以降は変更不要
const createUserUseCase = new CreateUserUseCase(userRepository);
const userController = new UserController(createUserUseCase, ...);
ディレクトリ構成
完成したアプリケーションのディレクトリ構成は以下のようになります。
src/
├── domain/ # ドメイン層
│ ├── entities/
│ │ └── User.ts
│ ├── value-objects/
│ │ ├── Email.ts
│ │ ├── UserName.ts
│ │ └── UserId.ts
│ └── repositories/
│ └── IUserRepository.ts
├── application/ # アプリケーション層
│ └── usecases/
│ ├── CreateUserUseCase.ts
│ ├── GetUserUseCase.ts
│ ├── GetAllUsersUseCase.ts
│ ├── UpdateUserUseCase.ts
│ └── DeleteUserUseCase.ts
├── infrastructure/ # インフラ層
│ └── prisma/
│ └── PrismaUserRepository.ts
├── presentation/ # プレゼンテーション層
│ ├── controllers/
│ │ └── UserController.ts
│ └── routes/
│ └── userRoutes.ts
├── middlewares/
│ └── validate.middleware.ts
├── schemas/
│ └── user.schema.ts
└── index.ts # エントリーポイント
まとめ
全7回にわたり、DDDを使ったリファクタリングについて解説してきました。
各回の内容
- Part1: 値オブジェクトの作成
- Part2: エンティティの作成
- Part3: リポジトリインターフェースの定義
- Part4: ユースケースの作成
- Part5: リポジトリの実装
- Part6: コントローラーの実装
- Part7: 依存性注入の設定(今回)
DDDの重要な概念
| 概念 | 説明 | 配置される層 |
|---|---|---|
| 値オブジェクト | 値そのものを表すオブジェクト | ドメイン層 |
| エンティティ | 一意の識別子を持つオブジェクト | ドメイン層 |
| リポジトリ | データ永続化の抽象化 | ドメイン層(インターフェース) インフラ層(実装) |
| ユースケース | アプリケーションの具体的な操作 | アプリケーション層 |
| コントローラー | HTTPリクエスト/レスポンスの処理 | プレゼンテーション層 |
| 依存性注入 | インスタンスを外部から渡す | エントリーポイント |
DDDのメリットとデメリット
DDDを適用することで以下のメリットがあります。
- ビジネスルールが明確になる
- 値オブジェクトとエンティティでドメイン知識を表現できる
- 「ユーザーとは何か」「メールアドレスとは何か」が明確になる
- 保守性が向上する
- 各層が独立しているため、変更の影響範囲が限定的
- データベースを変更してもドメイン層は影響を受けない
- テストしやすくなる
- リポジトリをモックに差し替えやすい設計
- ユースケース単位でテストできる
- チーム開発がしやすい
- 責務が明確なので、並行作業がしやすくなる
一方、以下のようなデメリットがあります。
- コード量が増える
- 層を分離することで、ファイル数とコード量が増える
- 学習コストがかかる
- DDDの概念(エンティティ、値オブジェクト、リポジトリなど)を理解する必要がある
そのため、以下の場合は過剰設計になる可能性があります。
- シンプルなCRUDアプリケーション
- 短期間で使い捨てのプロトタイプ
- 個人の小規模プロジェクト
そのため、今回のようなシンプルなCRUDアプリケーションでは過剰設計ですが、DDDの各概念を理解する良い練習になったと思います。