🤔第10章 テストに聞いてみる(興味深いtimes)
「システムの知識を総動員して検討することも可能ではあるが、私たちの手元にはきれいなコードがあり、 そのコードが動作しているという自信をテストが与えてくれる。検討に時間を割く代わりに、 コードを変更してテストを実行し、結果をコンピュータに教えてもらえばいい。」
「検討に時間を割く代わりに、コードを変更してテストを実行し、 結果をコンピュータに教えてもらえばいい。」
TODOリスト
- • `multiply`メソッドの興味深い実装
- •
型推論を活用したTODOアイテム変換
- •
テストから設計を導く手法
- •
`TodoItem`型と`TodoList`型の区別
⚠️現在の`multiply`実装の限界
現在のmultiply
実装は 単純にTODOアイテムを複製するだけです。 しかし、実際のアプリケーションでは、より興味深い動作が求められるかもしれません。 テストに聞いて、設計を導いてみましょう。
🔴1. レッド:興味深い`multiply`のテストを書く
まず、期待する動作をテストで表現してみます。TodoList
という新しい概念が必要かもしれません。
describe('興味深いmultiply操作のテスト', () => {
it('TODOアイテムを乗算してTodoListを返すこと', () => {
const learningTodo = TodoItem.learning(1, 'TypeScriptを練習する');
const todoList = learningTodo.multiplyToList(3);
expect(todoList.size()).toBe(3);
expect(todoList.getCategory()).toBe('LEARNING');
expect(todoList.getAll().every(todo =>
todo.text === 'TypeScriptを練習する'
)).toBe(true);
});
it('乗算時に異なるTODOタイプを作成すること', () => {
const workTodo = TodoItem.work(1, 'コードレビュー');
// Workカテゴリーは違うIDを持つべき
const multipliedTodos = workTodo.multiply(2);
expect(multipliedTodos[0].id).not.toBe(multipliedTodos[1].id);
expect(multipliedTodos[0].equals(multipliedTodos[1])).toBe(false);
});
});
このテストはTodoList
という 新しいクラスを要求しています。テストに聞いてみましょう -TodoList
が必要でしょうか?
❓2. テストに聞く:`TodoList`は`TodoItem`と同じか?
実験的なテストを書いて、システムの動作を確認します。
describe('TodoListの実験テスト', () => {
it('TodoListはMoneyのように振る舞うべきか?', () => {
const workTodo = TodoItem.work(1, 'アプリをデプロイする');
const todoList = workTodo.multiplyToList(1);
// TodoListは単一のTodoItemと等価であるべきか?
expect(todoList.equals(workTodo)).toBe(true);
});
it('乗算のエッジケースを処理すること', () => {
const personalTodo = TodoItem.personal(5, '牛乳を買う');
// ゼロでの乗算
const zeroResult = personalTodo.multiplyToList(0);
expect(zeroResult.size()).toBe(0);
// 負数での乗算
const negativeResult = personalTodo.multiplyToList(-1);
expect(negativeResult.size()).toBe(0);
});
});
🟢3. グリーン:最小限の`TodoList`実装
テストを実行すると失敗します。まだTodoList
クラスが 存在しないからです。実装してみましょう。
export interface ITodoList {
size(): number;
getCategory(): string;
getAll(): ITodoItem[];
equals(other: ITodoItem | ITodoList): boolean;
}
export class TodoList implements ITodoList {
constructor(
private items: ITodoItem[],
private category: string
) {}
size(): number {
return this.items.length;
}
getCategory(): string {
return this.category;
}
getAll(): ITodoItem[] {
return [...this.items]; // 防御的コピー
}
equals(other: ITodoItem | ITodoList): boolean {
if (other instanceof TodoList) {
return this.category === other.category &&
this.size() === other.size() &&
this.items.every((item, index) =>
item.equals(other.items[index])
);
}
// TodoItemとの比較: サイズ1でアイテムが等しい場合のみtrue
if (this.size() === 1) {
return this.items[0].equals(other);
}
return false;
}
// Factory Method
static fromItem(item: ITodoItem, count: number = 1): TodoList {
if (count <= 0) {
return new TodoList([], item.category);
}
const items = Array.from({ length: count }, (_, i) =>
TodoItem.create(
item.id + i,
item.text,
item.category as any
)
);
return new TodoList(items, item.category);
}
}
🔵4. リファクタリング:型推論の活用
TypeScriptの型推論を活用して、より安全で使いやすいAPIを提供します。
export class TodoList implements ITodoList {
// ... 既存のメソッド
equals(other: ITodoItem | ITodoList): boolean {
if (this.isTodoList(other)) {
return this.compareWithTodoList(other);
}
return this.compareWithTodoItem(other);
}
private isTodoList(obj: ITodoItem | ITodoList): obj is ITodoList {
return 'size' in obj && 'getCategory' in obj;
}
private compareWithTodoList(other: ITodoList): boolean {
return this.category === other.getCategory() &&
this.size() === other.size() &&
this.items.every((item, index) =>
item.equals(other.getAll()[index])
);
}
private compareWithTodoItem(other: ITodoItem): boolean {
return this.size() === 1 &&
this.items[0].equals(other);
}
}
🟢5. `TodoItem`に`multiplyToList`メソッドを追加
export interface ITodoItem {
readonly id: number;
readonly text: string;
readonly completed: boolean;
readonly category: string;
complete(): ITodoItem;
multiply(count: number): ITodoItem[];
multiplyToList(count: number): ITodoList; // 新しいメソッド
equals(other: ITodoItem): boolean;
}
// 実装クラスに追加
class IncompleteTodoItem extends TodoItem {
// ... 既存のコード
multiplyToList(count: number): ITodoList {
return TodoList.fromItem(this, count);
}
}
class CompletedTodoItem extends TodoItem {
// ... 既存のコード
multiplyToList(count: number): ITodoList {
// 完了済みアイテムは自分自身のリストを返す
return TodoList.fromItem(this, 1);
}
}
🧪6. テストに聞く:異なる型のTODOを区別できるか?
実験的なテストを書いて、システムの動作を確認します。
describe('TodoItemタイプの実験テスト', () => {
it('TodoItemは適切な場合にTodoListを返すこと', () => {
const workTodo = TodoItem.work(1, 'PRをレビューする');
const result = workTodo.multiplyToList(1);
// 結果は TodoList 型であるべき
expect(result).toBeInstanceOf(TodoList);
// しかし、単一のTodoItemと等価であるべき
expect(result.equals(workTodo)).toBe(true);
});
it('複雑な乗算シナリオを処理すること', () => {
const dailyStandup = TodoItem.work(1, '朝会');
// 一週間分の朝会TODO作成
const weeklyStandups = dailyStandup.multiplyToList(5);
expect(weeklyStandups.size()).toBe(5);
expect(weeklyStandups.getCategory()).toBe('WORK');
weeklyStandups.getAll().forEach((todo, index) => {
expect(todo.text).toBe('朝会');
expect(todo.id).toBe(1 + index);
});
});
});
⚛️7. React コンポーネントでの活用
"use client";
import React from 'react';
import { ITodoList } from '@/lib/TodoList';
import { TodoItemComponent } from './TodoItemComponent';
interface TodoListComponentProps {
todoList: ITodoList;
}
export function TodoListComponent({ todoList }: TodoListComponentProps) {
return (
<div className="bg-white rounded-lg shadow p-4">
<h3 className="text-lg font-semibold mb-3">
{todoList.getCategory()} ({todoList.size()} items)
</h3>
<ul className="space-y-2">
{todoList.getAll().map((todo, index) => (
<TodoItemComponent
key={`${todo.id}-${index}`}
todo={todo}
onToggle={() => {}}
/>
))}
</ul>
</div>
);
}
💡8. テストから学んだ設計判断
テストに聞いた結果、以下の設計判断を行いました:
`TodoList`の必要性
テストが`TodoList`型を要求し、`TodoItem`とは異なる動作が必要
等価性の定義
`TodoList`は条件によって`TodoItem`と等価になる
型安全性
TypeScriptの型ガードで安全な型判定を実現
Factory Methodの活用
一貫した生成パターンの提供
🚀9. Next.js App Routerでの統合
import { TodoListComponent } from '@/components/TodoListComponent';
import { TodoItem } from '@/lib/TodoItem';
export default function MultiplyDemoPage() {
const examples = [
TodoItem.work(1, 'コードレビュー').multiplyToList(3),
TodoItem.learning(10, 'TypeScript練習').multiplyToList(2),
TodoItem.personal(20, '運動').multiplyToList(5)
];
return (
<main className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Multiply Demo</h1>
<p className="text-gray-600 mb-8">
テストに聞いて設計した multiply 機能のデモンストレーション
</p>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{examples.map((todoList, index) => (
<TodoListComponent key={index} todoList={todoList} />
))}
</div>
</main>
);
}
📚テストに聞く手法の学び
この章で学んだ「テストに聞く」手法:
実験的なテストを書く
期待する動作が正しいかテストで確認
設計の疑問をテストで解決
コードで答えを見つける
型システムの活用
TypeScriptの型推論で安全な設計
小さな変更で大きな気づき
テストが設計の方向性を示す
TODOリストの更新
- • ✅ `multiply`メソッドの興味深い実装
- • ✅
型推論を活用したTODOアイテム変換
- • ✅
テストから設計を導く手法
- • ✅
`TodoItem`型と`TodoList`型の区別
🎯まとめ
「テストに聞いてみる」手法を使って、TodoList
という 新しい概念を発見し、より洗練された設計を導き出しました。 テストが設計の方向性を示し、コードで答えを見つけることで、 効果的な設計判断ができるようになります。 次の章では、「不要になったら消す」原則を学び、 重複コードの発見と除去を通じてクリーンな設計を実現します。