← トップページに戻る

TDD実践ガイド

🏭第8章 実装を隠す(ファクトリーメソッド)

TODOアプリの実装が進むにつれて、重複したコードが見えてきました。 実装を隠蔽するファクトリーメソッドパターンを導入し、より洗練された設計を実現します。

「2つのサブクラス`Dollar`と`Franc`にはもうコンストラクタしか残っていない。 コンストラクタだけのサブクラスを残しておく理由はないので、サブクラスを消してしまいたい。」

— Kent Beck『テスト駆動開発』より

TODOリスト

  • `TodoItem`と`CompletedTodoItem`の重複
  • `TodoItem`のサブクラスを隠す
  • TypeScriptの型安全性を保つ
  • 実装を隠すファクトリーメソッド

🔍現在の実装の問題点

第7章までで、基本的なTODOアイテムのクラス構造ができました。 しかし、TodoItemCompletedTodoItemの間に重複コードが存在し、 さらにクライアントコードが具体的なサブクラスを知りすぎています。

lib/TodoItem.ts
export class TodoItem {
  constructor(
    public id: number,
    public text: string,
    public completed: boolean = false
  ) {}

  complete(): TodoItem {
    return new CompletedTodoItem(this.id, this.text);
  }

  equals(other: TodoItem): boolean {
    return this.id === other.id && 
           this.text === other.text && 
           this.completed === other.completed;
  }
}

export class CompletedTodoItem extends TodoItem {
  constructor(id: number, text: string) {
    super(id, text, true);
  }

  complete(): TodoItem {
    return this; // 既に完了済み
  }
}

🔴1. レッド:Factory Methodを使うテストを書く

まず、TodoItemクラスに Factory Methodを導入し、クライアントコードがサブクラスを直接知らなくて済むようにします。

lib/TodoItem.test.ts
describe('TodoItemファクトリーメソッドのテスト', () => {
  it('ファクトリーメソッドを使用してTODOアイテムを作成すること', () => {
    const todo = TodoItem.create(1, 'ファクトリーパターンを学ぶ');
    expect(todo.id).toBe(1);
    expect(todo.text).toBe('ファクトリーパターンを学ぶ');
    expect(todo.completed).toBe(false);
  });

  it('ファクトリーメソッドを使用して完了済みTODOアイテムを作成すること', () => {
    const todo = TodoItem.createCompleted(1, '既に学習済み');
    expect(todo.id).toBe(1);
    expect(todo.text).toBe('既に学習済み');
    expect(todo.completed).toBe(true);
  });
});

このテストは失敗します。Factory Methodが存在しないためです。

🟢2. グリーン:Factory Methodを実装する

lib/TodoItem.ts
export abstract class TodoItem {
  constructor(
    public id: number,
    public text: string,
    public completed: boolean
  ) {}

  abstract complete(): TodoItem;

  equals(other: TodoItem): boolean {
    return this.id === other.id && 
           this.text === other.text && 
           this.completed === other.completed;
  }

  // Factory Methods
  static create(id: number, text: string): TodoItem {
    return new IncompleteTodoItem(id, text);
  }

  static createCompleted(id: number, text: string): TodoItem {
    return new CompletedTodoItem(id, text);
  }
}

class IncompleteTodoItem extends TodoItem {
  constructor(id: number, text: string) {
    super(id, text, false);
  }

  complete(): TodoItem {
    return TodoItem.createCompleted(this.id, this.text);
  }
}

class CompletedTodoItem extends TodoItem {
  constructor(id: number, text: string) {
    super(id, text, true);
  }

  complete(): TodoItem {
    return this; // 既に完了済み
  }
}

🔵3. リファクタリング:TypeScript型定義の改善

Factory Methodが通る状態になったので、より良い設計に向けてリファクタリングします。 TypeScriptの型システムを活用し、より安全で使いやすいAPIを提供します。

lib/TodoItem.ts (改良版)
export interface ITodoItem {
  readonly id: number;
  readonly text: string;
  readonly completed: boolean;
  complete(): ITodoItem;
  equals(other: ITodoItem): boolean;
}

export abstract class TodoItem implements ITodoItem {
  constructor(
    public readonly id: number,
    public readonly text: string,
    public readonly completed: boolean
  ) {}

  abstract complete(): ITodoItem;

  equals(other: ITodoItem): boolean {
    return this.id === other.id && 
           this.text === other.text && 
           this.completed === other.completed;
  }

  // Factory Methods - 実装を隠蔽
  static create(id: number, text: string): ITodoItem {
    return new IncompleteTodoItem(id, text);
  }

  static createCompleted(id: number, text: string): ITodoItem {
    return new CompletedTodoItem(id, text);
  }
}

⚛️4. Next.js App Routerでの実践応用

Factory Methodを使用して、Server ComponentからTODOアイテムを生成します。

app/todos/page.tsx
import { TodoList } from '@/components/TodoList';
import { TodoItem } from '@/lib/TodoItem';

// サンプルデータをFactory Methodで生成
function getSampleTodos() {
  return [
    TodoItem.create(1, 'TypeScriptでFactory Methodを学ぶ'),
    TodoItem.createCompleted(2, 'TDDの基本サイクルを理解する'),
    TodoItem.create(3, 'Next.js App Routerでコンポーネントを実装する'),
    TodoItem.createCompleted(4, 'ファクトリーメソッドパターンを適用する')
  ];
}

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>
      <TodoList 
        todos={todos} 
        onToggle={(id) => {
          // Client Componentでハンドリング
          console.log(`Toggle todo ${id}`);
        }} 
      />
    </main>
  );
}

設計の利点

Factory Methodパターンを導入したことで、以下の利点が得られました:

実装の隠蔽

クライアントコードは具体的なサブクラスを知る必要がない

型安全性

TypeScriptの`ITodoItem`インターフェースにより、型安全性が保たれる

拡張性

新しいTODOアイテムの種類を追加する際、Factory Methodを変更するだけで済む

テスタビリティ

Factory Methodをモックしやすく、テストが書きやすい

TODOリストの更新

  • `TodoItem`と`CompletedTodoItem`の重複
  • `TodoItem`のサブクラスを隠す
  • TypeScriptの型安全性を保つ
  • 実装を隠すファクトリーメソッド

🎯まとめ

Factory Methodパターンを使って実装の詳細を隠蔽し、より洗練された設計を実現しました。 次の章では、TDDにおける「歩幅の調整」について学び、times実装の一般化を通じて より洗練された設計手法を身につけます。