← トップページに戻る

TDD実践ガイド

👣第9章 歩幅の調整(times実装の一般化)

TDDでは、実装の歩幅を適切に調整することが重要です。 不安を感じたときは小さなステップで、自信があるときは大きなステップで進みます。 この章では、TODOアイテムの操作メソッドを一般化し、リファクタリングの歩幅調整を実践します。

「不安を感じたときは小さなステップで、自信があるときは大きなステップで進む。 TDDの魅力の一つは、歩幅を自分で決められることです。」

— TDD実践における歩幅調整の原則より

TODOリスト

  • TODOアイテムの操作メソッド一般化
  • `multiply`メソッドによる複数TODO作成
  • TypeScriptの通貨概念をTODOカテゴリーに適応
  • 実装歩幅の適切な調整

💡通貨の概念をTODOカテゴリーに適応

Kent Beckの原著では通貨(Currency)の概念が導入されました。 TODOアプリでは、これを「カテゴリー」として適応し、 異なる種類のタスクを区別して管理できるようにします。

🔴1. レッド:TODOカテゴリーのテストを書く

まず、TODOアイテムにカテゴリーの概念を導入します。小さなステップから始めます。

lib/TodoItem.test.ts
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. グリーン:小さなステップで実装する

最初は仮実装から始めます。不安があるので、小さな歩幅で進みます。

歩幅調整のポイント:新しい概念(カテゴリー)を導入するときは、まず仮実装で動作を確認してから一般化する

lib/TodoItem.ts (小さなステップ)
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を一般化し、より洗練された実装に変更します。

歩幅拡大のポイント:テストが安全網となっているので、大胆なリファクタリングができる

lib/TodoItem.ts (一般化)
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を作成する機能を実装します。

lib/TodoItem.test.ts
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. グリーン:一般化された実装

歩幅を調整して、より汎用的な実装を行います。

lib/TodoItem.ts
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コンポーネントを作成します。

components/TodoItemComponent.tsx
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メソッドを 実際のアプリケーションで活用する例を示します。

app/todos/page.tsx
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における歩幅調整の技術を習得しました。 不安があるときは小さなステップで、自信があるときは大きなステップで開発を進めることで、 効率的で安全なリファクタリングが可能になります。 次の章では、「テストに聞いてみる」手法を学び、 より興味深いtimesmultiply)実装を通じて テスト駆動設計の本質に迫ります。