🎭第12章 設計とメタファー(TODOリストの追加)
この章では、TODOアプリの最終章として、「Expression(式)」のメタファーを使って より洗練されたドメインモデルを構築します。数学的な式のように操作を組み合わせ可能な設計を実現します。
「オブジェクトたちに頼ろう。あるオブジェクトが望むように振る舞えないのならば、 同じ外部プロトコル(メソッド群)を備える新たなオブジェクト(Imposterと呼ばれる)を実装し、 仕事をさせればよい。」
TODOリスト
- • 式のメタファーをTODOアプリに適応
- •
TODOコレクションの抽象化
- •
`TaskManager`による操作の統一
- •
ドメインモデルの完成
🧮Expression メタファーの適応
Kent Beckの原著では「($2 + 3 CHF) * 5
」のような 式を表現しました。TODOアプリでは、これを「TODO操作の組み合わせ」として解釈します。
例:TODO操作の式
(work_tasks + personal_tasks).filter(incomplete).priority(high)
🔴1. レッド:TODO Expression のテストを書く
import { TodoExpression } from './TodoExpression';
import { TaskManager } from './TaskManager';
import { TodoItem } from './TodoItem';
describe('TodoExpressionのテスト', () => {
it('数学的な式のようにTODOコレクションを組み合わせること', () => {
const workTodos = TodoExpression.fromItems([
TodoItem.work(1, 'アプリをデプロイする'),
TodoItem.work(2, 'コードレビュー')
]);
const personalTodos = TodoExpression.fromItems([
TodoItem.personal(3, '食材を買う'),
TodoItem.personal(4, '運動する')
]);
const combined = workTodos.plus(personalTodos);
const taskManager = new TaskManager();
const result = taskManager.evaluate(combined);
expect(result.length).toBe(4);
expect(result.some(todo => todo.category === 'WORK')).toBe(true);
expect(result.some(todo => todo.category === 'PERSONAL')).toBe(true);
});
});
🟢2. グリーン:Expression インターフェースの実装
export interface ITodoExpression {
plus(other: ITodoExpression): ITodoExpression;
filter(predicate: (todo: ITodoItem) => boolean): ITodoExpression;
evaluate(manager: ITaskManager): ITodoItem[];
}
export interface ITaskManager {
evaluate(expression: ITodoExpression): ITodoItem[];
}
export class TodoExpression implements ITodoExpression {
constructor(private items: ITodoItem[]) {}
plus(other: ITodoExpression): ITodoExpression {
return new TodoSum(this, other);
}
filter(predicate: (todo: ITodoItem) => boolean): ITodoExpression {
return new TodoFilter(this, predicate);
}
evaluate(manager: ITaskManager): ITodoItem[] {
return [...this.items]; // 防御的コピー
}
static fromItems(items: ITodoItem[]): ITodoExpression {
return new TodoExpression(items);
}
}
// Composite パターンの実装
class TodoSum implements ITodoExpression {
constructor(
private left: ITodoExpression,
private right: ITodoExpression
) {}
plus(other: ITodoExpression): ITodoExpression {
return new TodoSum(this, other);
}
filter(predicate: (todo: ITodoItem) => boolean): ITodoExpression {
return new TodoFilter(this, predicate);
}
evaluate(manager: ITaskManager): ITodoItem[] {
const leftResult = this.left.evaluate(manager);
const rightResult = this.right.evaluate(manager);
return [...leftResult, ...rightResult];
}
}
🟢3. TaskManager の実装
export class TaskManager implements ITaskManager {
private priorities: Map<string, number> = new Map();
evaluate(expression: ITodoExpression): ITodoItem[] {
return expression.evaluate(this);
}
setPriority(category: string, priority: number): void {
this.priorities.set(category, priority);
}
getPriority(category: string): number {
return this.priorities.get(category) || 0;
}
// ヘルパーメソッド
sortByPriority(todos: ITodoItem[]): ITodoItem[] {
return [...todos].sort((a, b) => {
const aPriority = this.getPriority(a.category);
const bPriority = this.getPriority(b.category);
return bPriority - aPriority; // 高優先度が先
});
}
// 共通フィルター
static incomplete = (todo: ITodoItem) => !todo.completed;
static completed = (todo: ITodoItem) => todo.completed;
static byCategory = (category: string) => (todo: ITodoItem) =>
todo.category === category;
}
🔵4. リファクタリング:チェーン可能なAPIの設計
export class TodoExpression implements ITodoExpression {
constructor(private items: ITodoItem[]) {}
plus(other: ITodoExpression): ITodoExpression {
return new TodoSum(this, other);
}
filter(predicate: (todo: ITodoItem) => boolean): ITodoExpression {
return new TodoFilter(this, predicate);
}
// チェーン可能なAPI
incomplete(): ITodoExpression {
return this.filter(TaskManager.incomplete);
}
completed(): ITodoExpression {
return this.filter(TaskManager.completed);
}
category(cat: string): ITodoExpression {
return this.filter(TaskManager.byCategory(cat));
}
// 数学的操作のメタファー
multiply(count: number): ITodoExpression {
const multipliedItems: ITodoItem[] = [];
this.items.forEach(item => {
multipliedItems.push(...item.multiply(count));
});
return new TodoExpression(multipliedItems);
}
// Factory Methods
static work(...items: ITodoItem[]): ITodoExpression {
return new TodoExpression(items);
}
static personal(...items: ITodoItem[]): ITodoExpression {
return new TodoExpression(items);
}
static fromItems(items: ITodoItem[]): ITodoExpression {
return new TodoExpression(items);
}
}
🧪5. 実際の使用例テスト
describe('TodoExpression 高度な使用例のテスト', () => {
it('数学的な式のような複雑な表現を処理できること', () => {
const workTodos = TodoExpression.work(
TodoItem.work(1, 'デプロイ'),
TodoItem.work(2, 'レビュー', true) // 完了済み
);
const personalTodos = TodoExpression.personal(
TodoItem.personal(3, '運動'),
TodoItem.personal(4, '買い物', true) // 完了済み
);
// 式: (work + personal).incomplete()
const expression = workTodos
.plus(personalTodos)
.incomplete();
const taskManager = new TaskManager();
const result = taskManager.evaluate(expression);
expect(result).toHaveLength(2);
expect(result.every(todo => !todo.completed)).toBe(true);
});
it('数学的な操作のように式を乗算できること', () => {
const dailyTasks = TodoExpression.fromItems([
TodoItem.work(1, '朝会')
]);
// 式: dailyTasks * 5 (一週間分)
const weeklyTasks = dailyTasks.multiply(5);
const taskManager = new TaskManager();
const result = taskManager.evaluate(weeklyTasks);
expect(result).toHaveLength(5);
expect(result.every(todo => todo.text === '朝会')).toBe(true);
});
});
⚛️6. React コンポーネントでの Expression 活用
"use client";
import React, { useState, useMemo } from 'react';
import { TodoExpression } from '@/lib/TodoExpression';
import { TaskManager } from '@/lib/TaskManager';
import { TodoItem } from '@/lib/TodoItem';
export function ExpressionBuilder() {
const [includeWork, setIncludeWork] = useState(true);
const [includePersonal, setIncludePersonal] = useState(true);
const [onlyIncomplete, setOnlyIncomplete] = useState(false);
const result = useMemo(() => {
let expression = TodoExpression.fromItems([]);
if (includeWork) {
const workTodos = TodoExpression.fromItems([
TodoItem.work(1, '本番環境にデプロイ'),
TodoItem.work(2, 'コードレビュー', true)
]);
expression = expression.plus(workTodos);
}
if (includePersonal) {
const personalTodos = TodoExpression.fromItems([
TodoItem.personal(3, '朝のジョギング'),
TodoItem.personal(4, '食材の買い物', true)
]);
expression = expression.plus(personalTodos);
}
if (onlyIncomplete) {
expression = expression.incomplete();
}
const taskManager = new TaskManager();
taskManager.setPriority('WORK', 3);
taskManager.setPriority('PERSONAL', 1);
const evaluated = taskManager.evaluate(expression);
return taskManager.sortByPriority(evaluated);
}, [includeWork, includePersonal, onlyIncomplete]);
return (
<div className="space-y-6">
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="text-lg font-semibold mb-4">Expression Builder</h3>
<div className="text-sm text-gray-600 mb-4">
Expression: {`(${[
includeWork && 'work',
includePersonal && 'personal'
].filter(Boolean).join(' + ')})${onlyIncomplete ? '.incomplete()' : ''}`}
</div>
<div className="grid grid-cols-3 gap-4 mb-4">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={includeWork}
onChange={(e) => setIncludeWork(e.target.checked)}
/>
<span>Work Tasks</span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={includePersonal}
onChange={(e) => setIncludePersonal(e.target.checked)}
/>
<span>Personal Tasks</span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={onlyIncomplete}
onChange={(e) => setOnlyIncomplete(e.target.checked)}
/>
<span>Incomplete Only</span>
</label>
</div>
</div>
<div>
<h4 className="font-medium mb-3">Results ({result.length} items):</h4>
<ul className="space-y-2">
{result.map((todo) => (
<li key={todo.id} className="p-2 border rounded">
<span className={`${todo.completed ? 'line-through text-gray-500' : ''}`}>
{todo.text}
</span>
<span className="ml-2 text-xs bg-gray-200 px-2 py-1 rounded">
{todo.category}
</span>
</li>
))}
</ul>
</div>
</div>
);
}
🚀7. Next.js App Router での統合例
import { ExpressionBuilder } from '@/components/ExpressionBuilder';
export default function ExpressionDemoPage() {
return (
<main className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">TODO Expression Demo</h1>
<p className="text-gray-600 mb-8">
数学的な式のメタファーを使ったTODO操作のデモンストレーション
</p>
<ExpressionBuilder />
<div className="mt-12 bg-blue-50 p-6 rounded-lg">
<h2 className="text-xl font-semibold mb-4">Design Patterns Used</h2>
<ul className="space-y-2 text-sm">
<li><strong>Expression Pattern:</strong> 複雑な操作を組み合わせ可能な式として表現</li>
<li><strong>Composite Pattern:</strong> TodoSum, TodoFilterで階層構造を構築</li>
<li><strong>Strategy Pattern:</strong> TaskManagerで評価戦略を分離</li>
<li><strong>Builder Pattern:</strong> チェーン可能なAPIで式を構築</li>
</ul>
</div>
</main>
);
}
🧪8. 統合テスト
describe('TODO Expression 統合テスト', () => {
it('実際のシナリオを処理できること', () => {
// 実際のプロジェクト管理シナリオ
const sprintBacklog = TodoExpression.fromItems([
TodoItem.work(1, 'ユーザー認証の実装'),
TodoItem.work(2, 'データベーススキーマの設計'),
TodoItem.work(3, 'API ドキュメントの作成', true),
]);
const personalGoals = TodoExpression.fromItems([
TodoItem.learning(10, 'TypeScript をマスターする'),
TodoItem.personal(20, '運動習慣'),
]);
// 複雑な式: (sprint + goals).incomplete().priority_sorted
const expression = sprintBacklog
.plus(personalGoals)
.incomplete();
const taskManager = new TaskManager();
taskManager.setPriority('WORK', 10);
taskManager.setPriority('LEARNING', 5);
taskManager.setPriority('PERSONAL', 1);
const result = taskManager.evaluate(expression);
const sorted = taskManager.sortByPriority(result);
expect(sorted).toHaveLength(4); // 完了済みは除外
expect(sorted[0].category).toBe('WORK'); // 最高優先度
expect(sorted[sorted.length - 1].category).toBe('PERSONAL'); // 最低優先度
});
});
🏆設計の成果
Expression メタファーの導入により、以下を実現しました:
複雑な操作の組み合わせ
数学的な式のように操作を組み合わせ可能
型安全な設計
TypeScriptの型システムで安全性を保証
拡張可能な設計
新しい操作を簡単に追加可能
直感的なAPI
自然言語のような記述が可能
🏗️ドメインモデルの完成
この章で、TODOアプリのドメインモデルが完成しました:
TodoItem
個々のタスクを表現
TodoExpression
タスクの集合と操作を表現
TaskManager
評価とビジネスルールを管理
React Components
UIとドメインロジックを分離
TODOリストの更新
- • ✅ 式のメタファーをTODOアプリに適応
- • ✅
TODOコレクションの抽象化
- • ✅
`TaskManager`による操作の統一
- • ✅
ドメインモデルの完成
🎓第2編:設計編 完了
Kent Beckの「テスト駆動開発」第8章〜第12章の設計手法を、TODOアプリ開発に成功的に適応できました。 Factory Method、歩幅調整、テスト駆動設計、重複除去、そしてメタファーを活用した設計まで、 TDDの真髄を実践できました。
次の段階では、これらの設計パターンを実際のNext.js 15アプリケーションでさらに発展させ、 より大規模で実用的なTODOアプリケーションを構築していきます。
🎯まとめ
Expression メタファーを使って、数学的な式のように組み合わせ可能なTODO操作システムを構築しました。 Composite パターン、Strategy パターン、Builder パターンを組み合わせることで、 拡張可能で直感的なドメインモデルが完成しました。 これでTDD設計編の学習は完了です。実際のアプリケーション開発にこれらの手法を活用していきましょう。