SJ blog
architecture
A

信頼度ランク

S 公式ソース確認済み
A 成功実績多数・失敗例少数
B 賛否両論
C 動作未確認・セキュリティリスク高
Z 個人所感

デザインパターン入門:実務でよく使うGoFパターン5選

GoFデザインパターン23種の中から実務で特に頻出する5つを厳選。Strategy・Observer・Factory・Decorator・Commandをコード例付きで解説します。

一言結論

デザインパターンは「解法の名前」を共有するための語彙であり、Strategy・Observer・Factoryの3パターンだけでも習得すれば、コードレビューや設計議論での意思疎通コストが劇的に下がる。

GoFパターンとは

GoF(Gang of Four)の書籍『Design Patterns』(1994)で定義された23種のオブジェクト指向設計パターン。頻出する設計上の問題に名前をつけた共通語彙として、今もコードレビューや技術議論で使われている。

3カテゴリに分類:

  • 生成 (Creational): オブジェクト生成の方法
  • 構造 (Structural): クラスの組み合わせ方
  • 振る舞い (Behavioral): オブジェクト間の責務分担

1. Strategy パターン(振る舞い)

「アルゴリズムを差し替え可能にする」

// 課題: 支払い方法・ソートアルゴリズム・認証方式など、
//       処理の「中身」だけを切り替えたい

interface SortStrategy {
  sort(data: number[]): number[];
}

class BubbleSort implements SortStrategy {
  sort(data: number[]): number[] {
    // バブルソートの実装
    return [...data].sort((a, b) => a - b); // 簡略化
  }
}

class QuickSort implements SortStrategy {
  sort(data: number[]): number[] {
    // クイックソートの実装
    return [...data].sort((a, b) => a - b); // 簡略化
  }
}

class Sorter {
  constructor(private strategy: SortStrategy) {}

  // 実行時にストラテジーを切り替え可能
  setStrategy(strategy: SortStrategy) {
    this.strategy = strategy;
  }

  sort(data: number[]): number[] {
    return this.strategy.sort(data);
  }
}

const sorter = new Sorter(new BubbleSort());
sorter.sort([3, 1, 4, 1, 5]);

// データが多い場合はQuickSortに切り替え
sorter.setStrategy(new QuickSort());
sorter.sort(largeDataSet);

実務での使いどころ: 支払い方法・ファイルエクスポート形式(CSV/JSON/PDF)・認証プロバイダーの切り替え


2. Observer パターン(振る舞い)

「イベントの発行者と受信者を疎結合にする」

// 課題: 注文が完了したら、在庫更新・メール送信・ポイント付与など
//       複数の処理を呼び出したいが、注文処理に直接書きたくない

interface Observer<T> {
  update(event: T): void;
}

class EventEmitter<T> {
  private observers: Observer<T>[] = [];

  subscribe(observer: Observer<T>) {
    this.observers.push(observer);
  }

  unsubscribe(observer: Observer<T>) {
    this.observers = this.observers.filter(o => o !== observer);
  }

  emit(event: T) {
    this.observers.forEach(observer => observer.update(event));
  }
}

// イベントの型
interface OrderEvent {
  orderId: string;
  userId: string;
  total: number;
}

// 各オブザーバー
class InventoryObserver implements Observer<OrderEvent> {
  update(event: OrderEvent) {
    console.log(`在庫更新: 注文${event.orderId}`);
  }
}

class EmailObserver implements Observer<OrderEvent> {
  update(event: OrderEvent) {
    console.log(`メール送信 to user:${event.userId}`);
  }
}

// 使用
const orderEmitter = new EventEmitter<OrderEvent>();
orderEmitter.subscribe(new InventoryObserver());
orderEmitter.subscribe(new EmailObserver());

// 注文完了時にemitするだけ(追加の処理はsubscribeするだけ)
orderEmitter.emit({ orderId: "ORD001", userId: "user1", total: 5000 });

Node.js の EventEmitter、DOM の addEventListener、RxJSの Observable はすべてObserverパターンの実装。


3. Factory / Factory Method パターン(生成)

「オブジェクト生成のロジックを隠蔽する」

// 課題: 条件に応じて異なるクラスのインスタンスを生成したいが、
//       呼び出し側に生成の詳細を知らせたくない

interface Notification {
  send(message: string): Promise<void>;
}

class EmailNotification implements Notification {
  constructor(private address: string) {}
  async send(message: string) {
    console.log(`Email to ${this.address}: ${message}`);
  }
}

class SlackNotification implements Notification {
  constructor(private webhookUrl: string) {}
  async send(message: string) {
    console.log(`Slack to ${this.webhookUrl}: ${message}`);
  }
}

class SMSNotification implements Notification {
  constructor(private phoneNumber: string) {}
  async send(message: string) {
    console.log(`SMS to ${this.phoneNumber}: ${message}`);
  }
}

// Factory: 生成ロジックを集約
class NotificationFactory {
  static create(config: { type: string; target: string }): Notification {
    switch (config.type) {
      case "email": return new EmailNotification(config.target);
      case "slack": return new SlackNotification(config.target);
      case "sms":   return new SMSNotification(config.target);
      default: throw new Error(`Unknown notification type: ${config.type}`);
    }
  }
}

// 呼び出し側はtypeを指定するだけ
const notifier = NotificationFactory.create({ type: "slack", target: "https://hooks.slack.com/..." });
await notifier.send("デプロイ完了!");

4. Decorator パターン(構造)

「既存クラスを変更せずに機能を動的に追加する」

// 課題: ログ・キャッシュ・認証チェックなど横断的な処理を
//       本来の処理に埋め込まずに追加したい

interface DataFetcher {
  fetch(id: string): Promise<unknown>;
}

class UserFetcher implements DataFetcher {
  async fetch(id: string) {
    // DBからユーザー取得(本来の処理)
    return { id, name: "Alice" };
  }
}

// ログを追加するデコレータ
class LoggingDecorator implements DataFetcher {
  constructor(private wrapped: DataFetcher) {}

  async fetch(id: string) {
    console.time(`fetch:${id}`);
    const result = await this.wrapped.fetch(id);
    console.timeEnd(`fetch:${id}`);
    return result;
  }
}

// キャッシュを追加するデコレータ
class CachingDecorator implements DataFetcher {
  private cache = new Map<string, unknown>();
  constructor(private wrapped: DataFetcher) {}

  async fetch(id: string) {
    if (this.cache.has(id)) return this.cache.get(id);
    const result = await this.wrapped.fetch(id);
    this.cache.set(id, result);
    return result;
  }
}

// デコレータを重ねる(順番が重要)
const fetcher = new LoggingDecorator(
  new CachingDecorator(
    new UserFetcher()
  )
);

await fetcher.fetch("user123");  // ログ → キャッシュ確認 → DB取得

TypeScriptの @decorator 構文もこのパターン。ミドルウェアチェーン(Express・Hono等)も同じ発想。


5. Command パターン(振る舞い)

「操作をオブジェクトとして表現し、Undo・キューイング・ログを可能にする」

// 課題: テキストエディタやゲームで「元に戻す」「やり直す」を実装したい

interface Command {
  execute(): void;
  undo(): void;
}

// テキストエディタの例
class TextEditor {
  text = "";
}

class InsertTextCommand implements Command {
  constructor(
    private editor: TextEditor,
    private text: string,
    private position: number
  ) {}

  execute() {
    this.editor.text =
      this.editor.text.slice(0, this.position) +
      this.text +
      this.editor.text.slice(this.position);
  }

  undo() {
    this.editor.text =
      this.editor.text.slice(0, this.position) +
      this.editor.text.slice(this.position + this.text.length);
  }
}

// CommandHistory(UndoManager)
class CommandHistory {
  private history: Command[] = [];

  execute(command: Command) {
    command.execute();
    this.history.push(command);
  }

  undo() {
    const command = this.history.pop();
    command?.undo();
  }
}

// 使用例
const editor = new TextEditor();
const history = new CommandHistory();

history.execute(new InsertTextCommand(editor, "Hello", 0));
history.execute(new InsertTextCommand(editor, " World", 5));
console.log(editor.text); // "Hello World"

history.undo();
console.log(editor.text); // "Hello"

実務での使いどころ: Undo/Redo・トランザクションログ・タスクキュー・バッチ処理の遅延実行


まとめ

パターンカテゴリ解決する問題
Strategy振る舞いアルゴリズムの差し替え
Observer振る舞いイベント駆動・疎結合
Factory生成オブジェクト生成の隠蔽
Decorator構造機能の動的追加
Command振る舞い操作のオブジェクト化・Undo

パターンを知ることで、コードレビューで「これはStrategyパターンで設計できそう」と議論できるようになる。名前がついていると、複雑な設計を1単語で説明できるのが最大の価値。


参考: Refactoring.Guru デザインパターン / Design Patterns: Elements of Reusable Object-Oriented Software