LESSON 40分

ストーリー

高橋アーキテクト
テキストエディタで”元に戻す”ボタンを押したことはあるよね?
あなた
毎日使っています
高橋アーキテクト
あの Undo 機能は Command パターンで実装されていることが多い。操作そのものをオブジェクトにすることで、実行・取り消し・やり直しが自由にできる。そしてTemplate Methodは、処理の大枠をスーパークラスで定義し、具体的な処理をサブクラスに任せるパターンだ

Command パターン

目的

操作(リクエスト)をオブジェクトとして表現する。これにより、操作のキュー化、ログ記録、Undo/Redo が実現できる。

Command の実装:テキストエディタ

// Command インターフェース
interface Command {
  execute(): void;
  undo(): void;
  describe(): string;
}

// テキストエディタのモデル
class TextDocument {
  private content: string = '';

  getContent(): string { return this.content; }

  insertAt(position: number, text: string): void {
    this.content = this.content.slice(0, position) + text + this.content.slice(position);
  }

  deleteRange(position: number, length: number): string {
    const deleted = this.content.slice(position, position + length);
    this.content = this.content.slice(0, position) + this.content.slice(position + length);
    return deleted;
  }
}

// 具体的な Command:文字挿入
class InsertCommand implements Command {
  constructor(
    private document: TextDocument,
    private position: number,
    private text: string
  ) {}

  execute(): void {
    this.document.insertAt(this.position, this.text);
  }

  undo(): void {
    this.document.deleteRange(this.position, this.text.length);
  }

  describe(): string {
    return `Insert "${this.text}" at position ${this.position}`;
  }
}

// 具体的な Command:文字削除
class DeleteCommand implements Command {
  private deletedText: string = '';

  constructor(
    private document: TextDocument,
    private position: number,
    private length: number
  ) {}

  execute(): void {
    this.deletedText = this.document.deleteRange(this.position, this.length);
  }

  undo(): void {
    this.document.insertAt(this.position, this.deletedText);
  }

  describe(): string {
    return `Delete ${this.length} chars at position ${this.position}`;
  }
}

// Invoker:コマンドの実行管理
class CommandManager {
  private history: Command[] = [];
  private undoneCommands: Command[] = [];

  execute(command: Command): void {
    command.execute();
    this.history.push(command);
    this.undoneCommands = []; // 新しい操作をしたらRedo履歴をクリア
  }

  undo(): void {
    const command = this.history.pop();
    if (command) {
      command.undo();
      this.undoneCommands.push(command);
    }
  }

  redo(): void {
    const command = this.undoneCommands.pop();
    if (command) {
      command.execute();
      this.history.push(command);
    }
  }

  getHistory(): string[] {
    return this.history.map(cmd => cmd.describe());
  }
}

// 使い方
const doc = new TextDocument();
const manager = new CommandManager();

manager.execute(new InsertCommand(doc, 0, 'Hello'));
console.log(doc.getContent()); // 'Hello'

manager.execute(new InsertCommand(doc, 5, ' World'));
console.log(doc.getContent()); // 'Hello World'

manager.undo();
console.log(doc.getContent()); // 'Hello'

manager.redo();
console.log(doc.getContent()); // 'Hello World'

Command の応用:マクロ

// 複数のコマンドをまとめて実行
class MacroCommand implements Command {
  private commands: Command[] = [];

  add(command: Command): void {
    this.commands.push(command);
  }

  execute(): void {
    for (const cmd of this.commands) {
      cmd.execute();
    }
  }

  undo(): void {
    // 逆順で Undo
    for (let i = this.commands.length - 1; i >= 0; i--) {
      this.commands[i].undo();
    }
  }

  describe(): string {
    return `Macro: [${this.commands.map(c => c.describe()).join(', ')}]`;
  }
}

Template Method パターン

目的

処理のアルゴリズムの骨格を基底クラスで定義し、具体的なステップの実装をサブクラスに委ねる。処理の順序は固定しつつ、個々のステップをカスタマイズ可能にする。

Template Method の実装:データ処理パイプライン

// 抽象クラス:処理の骨格を定義
abstract class DataProcessor<TInput, TOutput> {
  // Template Method -- 処理の流れを定義(final にしたい)
  process(input: TInput): TOutput {
    console.log(`Processing started: ${this.getName()}`);
    const validated = this.validate(input);
    const transformed = this.transform(validated);
    const result = this.format(transformed);
    this.onComplete(result);
    console.log(`Processing completed: ${this.getName()}`);
    return result;
  }

  // サブクラスが実装すべきメソッド
  protected abstract getName(): string;
  protected abstract validate(input: TInput): TInput;
  protected abstract transform(input: TInput): unknown;
  protected abstract format(data: unknown): TOutput;

  // オプショナルなフック(必要に応じてオーバーライド)
  protected onComplete(result: TOutput): void {
    // デフォルトでは何もしない
  }
}

// CSV処理
class CsvProcessor extends DataProcessor<string, string[][]> {
  protected getName(): string { return 'CSV Processor'; }

  protected validate(input: string): string {
    if (!input.trim()) throw new Error('Empty CSV input');
    return input.trim();
  }

  protected transform(input: string): string[][] {
    return input.split('\n').map(line => line.split(','));
  }

  protected format(data: unknown): string[][] {
    const rows = data as string[][];
    return rows.map(row => row.map(cell => cell.trim()));
  }
}

// JSON処理
class JsonProcessor extends DataProcessor<string, Record<string, unknown>> {
  protected getName(): string { return 'JSON Processor'; }

  protected validate(input: string): string {
    try {
      JSON.parse(input);
    } catch {
      throw new Error('Invalid JSON');
    }
    return input;
  }

  protected transform(input: string): Record<string, unknown> {
    return JSON.parse(input);
  }

  protected format(data: unknown): Record<string, unknown> {
    return data as Record<string, unknown>;
  }

  protected onComplete(result: Record<string, unknown>): void {
    console.log(`Parsed ${Object.keys(result).length} keys`);
  }
}

// 使い方
const csvProcessor = new CsvProcessor();
const result1 = csvProcessor.process('name,age\nAlice,30\nBob,25');

const jsonProcessor = new JsonProcessor();
const result2 = jsonProcessor.process('{"name": "Takahashi", "role": "architect"}');

Template Method のもう1つの例:レポート生成

abstract class ReportGenerator {
  // Template Method
  generate(): string {
    const header = this.buildHeader();
    const body = this.buildBody();
    const footer = this.buildFooter();
    return `${header}\n${body}\n${footer}`;
  }

  protected abstract buildHeader(): string;
  protected abstract buildBody(): string;

  // フック:デフォルト実装あり(オーバーライド可能)
  protected buildFooter(): string {
    return `Generated at: ${new Date().toISOString()}`;
  }
}

class SalesReport extends ReportGenerator {
  protected buildHeader(): string {
    return '=== Monthly Sales Report ===';
  }

  protected buildBody(): string {
    return 'Total Sales: 1,234,567 JPY\nGrowth: +12%';
  }
}

class InventoryReport extends ReportGenerator {
  protected buildHeader(): string {
    return '=== Inventory Status ===';
  }

  protected buildBody(): string {
    return 'Total Items: 5,432\nLow Stock: 23 items';
  }

  protected buildFooter(): string {
    return `${super.buildFooter()}\nNext check: Tomorrow`;
  }
}

Command vs Template Method

観点CommandTemplate Method
目的操作をオブジェクト化処理の骨格を定義
柔軟性実行時に操作を組み立てコンパイル時にステップを定義
拡張方法新しい Command クラス追加サブクラスでステップを実装
典型的な用途Undo/Redo、キュー、マクロデータ処理、レポート生成

高橋アーキテクトのアドバイス:

「Command は”操作を記録する”ために使い、Template Method は”処理の流れを統一する”ために使う。どちらもフレームワーク設計で頻出するパターンだ」


まとめ

ポイント内容
Command操作をオブジェクトとして表現し、Undo/Redo を実現
Template Method処理の骨格を基底クラスで定義し、詳細をサブクラスへ
Command の効果操作の記録・再生・取り消しが可能
Template Method の効果処理の流れを統一しつつカスタマイズ可能

チェックリスト

  • Command パターンで Undo/Redo を実装できる
  • Template Method パターンで処理の骨格を定義できる
  • 2つのパターンの使い分けを判断できる

次のステップへ

次は演習です。Step 4 で学んだパターン(Adapter、Decorator、Facade、Composite、Strategy、Observer、Command、Template Method)を組み合わせて設計してみましょう。


推定読了時間: 40分