👣第9章 歩幅の調整(times実装の一般化)
TDDでは、実装の歩幅を適切に調整することが重要です。 不安を感じたときは小さなステップで、自信があるときは大きなステップで進みます。 この章では、TODOアイテムの操作メソッドを一般化し、リファクタリングの歩幅調整を実践します。
「不安を感じたときは小さなステップで、自信があるときは大きなステップで進む。 TDDの魅力の一つは、歩幅を自分で決められることです。」
TODOリスト
- • TODOアイテムの操作メソッド一般化
- •
`multiply`メソッドによる複数TODO作成
- •
TypeScriptの通貨概念をTODOカテゴリーに適応
- •
実装歩幅の適切な調整
💡通貨の概念をTODOカテゴリーに適応
Kent Beckの原著では通貨(Currency)の概念が導入されました。 TODOアプリでは、これを「カテゴリー」として適応し、 異なる種類のタスクを区別して管理できるようにします。
🔴1. レッド:TODOカテゴリーのテストを書く
まず、TODOアイテムにカテゴリーの概念を導入します。小さなステップから始めます。
describe('カテゴリー付きTodoItemのテスト', () => {
it('カテゴリープロパティを持つこと', () => {
const workTodo = TodoItem.work(1, 'プロジェクトを完了する');
const personalTodo = TodoItem.personal(1, '食材を買う');
expect(workTodo.category).toBe('WORK');
expect(personalTodo.category).toBe('PERSONAL');
});
it('カテゴリーが異なる場合は等価でないこと', () => {
const workTodo = TodoItem.work(1, 'タスク');
const personalTodo = TodoItem.personal(1, 'タスク');
expect(workTodo.equals(personalTodo)).toBe(false);
});
});
🟢2. グリーン:小さなステップで実装する
最初は仮実装から始めます。不安があるので、小さな歩幅で進みます。
歩幅調整のポイント:新しい概念(カテゴリー)を導入するときは、まず仮実装で動作を確認してから一般化する
type TodoCategory = 'WORK' | 'PERSONAL' | 'SHOPPING' | 'LEARNING';
export interface ITodoItem {
readonly id: number;
readonly text: string;
readonly completed: boolean;
readonly category: TodoCategory;
complete(): ITodoItem;
multiply(count: number): ITodoItem[];
equals(other: ITodoItem): boolean;
}
export abstract class TodoItem implements ITodoItem {
constructor(
public readonly id: number,
public readonly text: string,
public readonly completed: boolean,
public readonly category: TodoCategory
) {}
// Category Factory Methods - 仮実装
static work(id: number, text: string): ITodoItem {
return new IncompleteTodoItem(id, text, 'WORK');
}
static personal(id: number, text: string): ITodoItem {
return new IncompleteTodoItem(id, text, 'PERSONAL');
}
}
🔵3. リファクタリング:歩幅を大きくして一般化
テストが通ったので、自信を持って歩幅を大きくします。 Factory Methodを一般化し、より洗練された実装に変更します。
歩幅拡大のポイント:テストが安全網となっているので、大胆なリファクタリングができる
export abstract class TodoItem implements ITodoItem {
// 一般化されたFactory Method
static create(
id: number,
text: string,
category: TodoCategory = 'PERSONAL'
): ITodoItem {
return new IncompleteTodoItem(id, text, category);
}
static createCompleted(
id: number,
text: string,
category: TodoCategory = 'PERSONAL'
): ITodoItem {
return new CompletedTodoItem(id, text, category);
}
// 特定カテゴリー用のFactory Methods
static work(id: number, text: string): ITodoItem {
return TodoItem.create(id, text, 'WORK');
}
static personal(id: number, text: string): ITodoItem {
return TodoItem.create(id, text, 'PERSONAL');
}
static shopping(id: number, text: string): ITodoItem {
return TodoItem.create(id, text, 'SHOPPING');
}
static learning(id: number, text: string): ITodoItem {
return TodoItem.create(id, text, 'LEARNING');
}
}
🔴4. レッド:`multiply`メソッドのテスト強化
multiply
メソッドを より興味深いものにします。複数のTODOを作成する機能を実装します。
describe('TodoItem multiplyメソッドのテスト', () => {
it('複数の作業TODOを作成すること', () => {
const workTodo = TodoItem.work(1, 'コードレビューを行う');
const multipliedTodos = workTodo.multiply(3);
expect(multipliedTodos).toHaveLength(3);
expect(multipliedTodos[0].text).toBe('コードレビューを行う');
expect(multipliedTodos[0].category).toBe('WORK');
expect(multipliedTodos[1].id).toBe(2);
expect(multipliedTodos[2].id).toBe(3);
});
it('異なるIDで複数の学習TODOを作成すること', () => {
const learningTodo = TodoItem.learning(10, 'TypeScriptを練習する');
const multipliedTodos = learningTodo.multiply(2);
expect(multipliedTodos).toHaveLength(2);
multipliedTodos.forEach(todo => {
expect(todo.category).toBe('LEARNING');
expect(todo.completed).toBe(false);
});
});
});
🟢5. グリーン:一般化された実装
歩幅を調整して、より汎用的な実装を行います。
class IncompleteTodoItem extends TodoItem {
constructor(id: number, text: string, category: TodoCategory) {
super(id, text, false, category);
}
complete(): ITodoItem {
return TodoItem.createCompleted(this.id, this.text, this.category);
}
multiply(count: number): ITodoItem[] {
if (count <= 0) return [];
const result: ITodoItem[] = [];
for (let i = 0; i < count; i++) {
result.push(TodoItem.create(
this.id + i,
this.text,
this.category
));
}
return result;
}
}
🎨6. TODOリストUIコンポーネントの更新
カテゴリー情報を表示するUIコンポーネントを作成します。
const categoryStyles = {
WORK: 'bg-red-100 text-red-800',
PERSONAL: 'bg-green-100 text-green-800',
SHOPPING: 'bg-yellow-100 text-yellow-800',
LEARNING: 'bg-blue-100 text-blue-800'
};
export function TodoItemComponent({ todo, onToggle }) {
return (
<li className="flex items-center space-x-3 p-3 border rounded-lg">
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span className="flex-1">
{todo.text}
</span>
<span className={`px-2 py-1 text-xs rounded-full ${categoryStyles[todo.category]}`}>
{todo.category}
</span>
</li>
);
}
⚛️7. Next.js App Router統合
multiply
メソッドを 実際のアプリケーションで活用する例を示します。
function getSampleTodos() {
return [
TodoItem.work(1, 'プロジェクトのリリース準備'),
TodoItem.learning(2, 'TypeScriptの高度な型システム習得'),
TodoItem.personal(3, '健康診断の予約'),
TodoItem.shopping(4, '週末の食材購入'),
...TodoItem.work(5, 'コードレビュー').multiply(3) // multiply使用例
];
}
export default function TodosPage() {
const todos = getSampleTodos();
return (
<main className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">カテゴリー別 TODO List</h1>
<div className="mb-4">
<p className="text-gray-600">
作業: {todos.filter(t => t.category === 'WORK').length}件 |
学習: {todos.filter(t => t.category === 'LEARNING').length}件 |
個人: {todos.filter(t => t.category === 'PERSONAL').length}件 |
買い物: {todos.filter(t => t.category === 'SHOPPING').length}件
</p>
</div>
<TodoList todos={todos} onToggle={(id) => console.log(`Toggle ${id}`)} />
</main>
);
}
📚歩幅調整の学び
この章では以下の歩幅調整テクニックを学びました:
小さなステップから開始
不安があるときは仮実装から始める
自信がついたら大きなステップ
テストが通ったら一般化を進める
リファクタリング時の歩幅調整
安全な変更は一気に、リスクがある変更は慎重に
型システムの活用
TypeScriptの型を使って歩幅を安全に大きくできる
TODOリストの更新
- • ✅ TODOアイテムの操作メソッド一般化
- • ✅
`multiply`メソッドによる複数TODO作成
- • ✅
TypeScriptの通貨概念をTODOカテゴリーに適応
- • ✅
実装歩幅の適切な調整
🎯まとめ
TDDにおける歩幅調整の技術を習得しました。 不安があるときは小さなステップで、自信があるときは大きなステップで開発を進めることで、 効率的で安全なリファクタリングが可能になります。 次の章では、「テストに聞いてみる」手法を学び、 より興味深いtimes
(multiply
)実装を通じて テスト駆動設計の本質に迫ります。