ストーリー
演習の概要
Step 2で作成したタスク管理APIを、クリーンアーキテクチャの4層構造で再設計してください。
| 項目 | 内容 |
|---|---|
| テーマ | タスク管理APIの再構築 |
| アーキテクチャ | クリーンアーキテクチャ(4層) |
| 言語 | TypeScript |
| ミッション数 | 5 |
| 推定時間 | 60分 |
Mission 1: ディレクトリ構造を設計しよう(10分)
クリーンアーキテクチャの4層に対応したディレクトリ構造を設計してください。
解答例
src/
├── entities/ # Layer 1: Enterprise Business Rules
│ ├── Task.ts
│ ├── TaskId.ts
│ ├── TaskTitle.ts
│ └── TaskStatus.ts
├── use-cases/ # Layer 2: Application Business Rules
│ ├── CreateTaskUseCase.ts
│ ├── ListTasksUseCase.ts
│ ├── ChangeTaskStatusUseCase.ts
│ ├── dto/
│ │ ├── CreateTaskInput.ts
│ │ ├── CreateTaskOutput.ts
│ │ ├── ListTasksOutput.ts
│ │ ├── ChangeTaskStatusInput.ts
│ │ └── TaskDto.ts
│ └── ports/
│ └── TaskRepository.ts # Output Port (interface)
├── adapters/ # Layer 3: Interface Adapters
│ ├── controllers/
│ │ └── TaskController.ts
│ └── gateways/
│ ├── PrismaTaskRepository.ts
│ └── InMemoryTaskRepository.ts
├── frameworks/ # Layer 4: Frameworks & Drivers
│ ├── express/
│ │ └── router.ts
│ └── prisma/
│ └── schema.prisma
├── composition-root.ts # DI設定
└── main.ts # エントリーポイント
Mission 2: Use CaseのInput/Outputを定義しよう(10分)
各Use CaseのInput型とOutput型を定義してください。
解答例
// use-cases/dto/CreateTaskInput.ts
interface CreateTaskInput {
readonly title: string;
readonly description?: string;
}
// use-cases/dto/CreateTaskOutput.ts
interface CreateTaskOutput {
readonly taskId: string;
readonly createdAt: string;
}
// use-cases/dto/ChangeTaskStatusInput.ts
interface ChangeTaskStatusInput {
readonly taskId: string;
readonly action: 'start' | 'complete';
}
// use-cases/dto/TaskDto.ts
interface TaskDto {
readonly id: string;
readonly title: string;
readonly description: string | null;
readonly status: string;
readonly createdAt: string;
}
// use-cases/dto/ListTasksOutput.ts
interface ListTasksOutput {
readonly tasks: TaskDto[];
readonly totalCount: number;
}
Mission 3: Use Caseを実装しよう(15分)
Input/Outputの型を使ってUse Caseを実装してください。
解答例
// use-cases/CreateTaskUseCase.ts
class CreateTaskUseCase {
constructor(private taskRepo: TaskRepository) {}
async execute(input: CreateTaskInput): Promise<CreateTaskOutput> {
const task = Task.create(input.title, input.description);
await this.taskRepo.save(task);
return {
taskId: task.id.value,
createdAt: task.toPersistence().createdAt.toISOString(),
};
}
}
// use-cases/ListTasksUseCase.ts
class ListTasksUseCase {
constructor(private taskRepo: TaskRepository) {}
async execute(): Promise<ListTasksOutput> {
const tasks = await this.taskRepo.findAll();
return {
tasks: tasks.map(task => ({
id: task.id.value,
title: task.title.value,
description: task.toPersistence().description,
status: task.status,
createdAt: task.toPersistence().createdAt.toISOString(),
})),
totalCount: tasks.length,
};
}
}
// use-cases/ChangeTaskStatusUseCase.ts
class ChangeTaskStatusUseCase {
constructor(private taskRepo: TaskRepository) {}
async execute(input: ChangeTaskStatusInput): Promise<void> {
const taskId = TaskId.fromString(input.taskId);
const task = await this.taskRepo.findById(taskId);
if (!task) throw new Error('タスクが見つかりません');
if (input.action === 'start') {
task.startProgress();
} else if (input.action === 'complete') {
task.complete();
}
await this.taskRepo.save(task);
}
}
Mission 4: Composition Rootを実装しよう(10分)
本番用とテスト用のComposition Rootを実装してください。
解答例
// composition-root.ts(本番用)
function createProductionContainer() {
const prisma = new PrismaClient();
const taskRepo = new PrismaTaskRepository(prisma);
const createTask = new CreateTaskUseCase(taskRepo);
const listTasks = new ListTasksUseCase(taskRepo);
const changeStatus = new ChangeTaskStatusUseCase(taskRepo);
const taskController = new TaskController(createTask, listTasks, changeStatus);
return { taskController, prisma };
}
// test/helpers/createTestContainer.ts(テスト用)
function createTestContainer() {
const taskRepo = new InMemoryTaskRepository();
const createTask = new CreateTaskUseCase(taskRepo);
const listTasks = new ListTasksUseCase(taskRepo);
const changeStatus = new ChangeTaskStatusUseCase(taskRepo);
return {
createTask,
listTasks,
changeStatus,
taskRepo, // テストで状態を確認するために公開
};
}
Mission 5: 各層のテストを書こう(15分)
Entity(Layer 1)とUse Case(Layer 2)のテストを書いてください。
解答例
// Layer 1: Entityのテスト
describe('Task', () => {
it('タスクを作成できる', () => {
const task = Task.create('テストタスク', '説明文');
expect(task.title.value).toBe('テストタスク');
expect(task.status).toBe(TaskStatus.TODO);
});
it('タイトルが100文字を超えるとエラー', () => {
const longTitle = 'a'.repeat(101);
expect(() => Task.create(longTitle)).toThrow('100文字以内');
});
it('TODO→IN_PROGRESS→DONEの順にのみ遷移できる', () => {
const task = Task.create('タスク');
expect(() => task.complete()).toThrow(); // TODO→DONEは不可
task.startProgress();
expect(task.status).toBe(TaskStatus.IN_PROGRESS);
task.complete();
expect(task.status).toBe(TaskStatus.DONE);
});
});
// Layer 2: Use Caseのテスト
describe('CreateTaskUseCase', () => {
it('タスクを作成して保存する', async () => {
const container = createTestContainer();
const output = await container.createTask.execute({
title: '新しいタスク',
description: '詳細',
});
expect(output.taskId).toBeDefined();
expect(output.createdAt).toBeDefined();
// Repositoryに保存されていることを確認
const tasks = await container.taskRepo.findAll();
expect(tasks.length).toBe(1);
});
});
describe('ChangeTaskStatusUseCase', () => {
it('タスクのステータスを変更できる', async () => {
const container = createTestContainer();
const output = await container.createTask.execute({ title: 'タスク' });
await container.changeStatus.execute({
taskId: output.taskId,
action: 'start',
});
const task = await container.taskRepo.findById(
TaskId.fromString(output.taskId)
);
expect(task!.status).toBe(TaskStatus.IN_PROGRESS);
});
it('存在しないタスクはエラー', async () => {
const container = createTestContainer();
await expect(
container.changeStatus.execute({
taskId: 'nonexistent',
action: 'start',
})
).rejects.toThrow('タスクが見つかりません');
});
});
達成度チェック
| Mission | 内容 | 完了 |
|---|---|---|
| 1 | ディレクトリ構造の設計 | [ ] |
| 2 | Input/Outputの定義 | [ ] |
| 3 | Use Caseの実装 | [ ] |
| 4 | Composition Rootの実装 | [ ] |
| 5 | 各層のテスト | [ ] |
チェックリスト
- 4層構造のディレクトリが明確に分かれている
- Use CaseがInput/Output型を使っている
- Composition Rootで全ての依存が組み立てられている
- Entity層のテストが外部依存なしで実行できる
- Use Case層のテストがInMemory Adapterで実行できる
次のステップへ
演習お疲れさまでした。次はStep 3のチェックポイントクイズに挑戦しましょう。
推定所要時間: 60分