Template Method パターン

アルゴリズムの骨格を親クラスで定義し、具体的なステップをサブクラスに委ねるデザインパターン

設計パターンオブジェクト指向

Template Method パターンとは

Template Method パターンは、アルゴリズムの全体的な流れ (テンプレート) を親クラスで定義し、各ステップの具体的な実装をサブクラスに委ねるデザインパターンである。Erich Gamma・Richard Helm・Ralph Johnson・John Vlissides の 4 人が 1994 年に Addison-Wesley から刊行した『Design Patterns: Elements of Reusable Object-Oriented Software』(邦訳『オブジェクト指向における再利用のためのデザインパターン』) が挙げた 23 パターンのうち、振る舞いに関するパターンに分類される。

通常とは呼び出しの向きが逆になるのがこのパターンの本質である。サブクラスは親クラスの手順を呼び出すのではなく、親クラスのテンプレートメソッドから呼ばれる側に回る。この制御の反転 (Inversion of Control) は「ハリウッドの原則 (Don't call us, we'll call you)」とも呼ばれ、フレームワークが拡張可能な骨格として振る舞える根拠になっている。

基本的な実装

データの取得・変換・保存・通知という 4 段の流れは、出力先が変わっても順序は共通で各段の中身だけが違う。共通の流れを親クラスのテンプレートメソッドに固定し、変わる部分を抽象メソッドとして切り出す。通知のように「既定の動作で足りることが多いが差し替えたい場合もある」処理は、既定実装を持つフックメソッドにする。

// db (SQL クライアント)・fs (node:fs/promises)・ddb・s3・sns・topicArn は初期化済みとする
abstract class DataExporter {
  // テンプレートメソッド: アルゴリズムの骨格
  async export(): Promise<void> {
    const data = await this.fetchData();
    const transformed = this.transform(data);
    await this.save(transformed);
    await this.notify();
  }

  // 抽象メソッド: サブクラスが実装
  protected abstract fetchData(): Promise<any[]>;
  protected abstract transform(data: any[]): string;
  protected abstract save(content: string): Promise<void>;

  // フックメソッド: デフォルト実装あり、オーバーライド可能
  protected async notify(): Promise<void> {
    console.log('Export completed');
  }
}

class CsvExporter extends DataExporter {
  protected async fetchData() { return db.query('SELECT * FROM orders'); }
  protected transform(data: any[]) { return data.map(r => Object.values(r).join(',')).join('\n'); }
  protected async save(content: string) { await fs.writeFile('export.csv', content); }
}

class S3JsonExporter extends DataExporter {
  protected async fetchData() { const res = await ddb.scan({ TableName: 'Orders' }); return res.Items ?? []; }
  protected transform(data: any[]) { return JSON.stringify(data); }
  protected async save(content: string) { await s3.putObject({ Bucket: 'exports', Key: 'orders.json', Body: content }); }
  protected async notify() { await sns.publish({ TopicArn: topicArn, Message: 'S3 export done' }); }
}

Strategy パターンとの違い

どちらも変わる部分を外に出すパターンだが、差し替えの手段とタイミングが違う。

観点Template MethodStrategy
拡張の手段継承 (サブクラスがステップを実装)コンポジション (アルゴリズムのオブジェクトを注入)
差し替えの粒度アルゴリズムの一部のステップアルゴリズム全体
差し替えのタイミングサブクラスを選んだ時点で固定実行中に入れ替えられる
親子の結合サブクラスが親の protected メソッドに依存インターフェース経由で疎結合

Template Method は「アルゴリズムの骨格は同じで、一部のステップだけが異なる」場合に使う。骨格そのものが違うなら Strategy を選ぶ。

継承を使う代償として、Template Method には制約が 3 つある。第 1 に、多重継承のない言語では親クラスを 1 つしか選べない。第 2 に、親クラスの手順を変えると全サブクラスに影響が及ぶ。第 3 に、変化の軸が 2 つ以上あるとサブクラスが軸の組み合わせの数だけ増える。出力先とフォーマットが独立に増えていくような場合は、サブクラスを掛け算で作るより、Strategy か次に示す関数の合成で軸ごとに分けたほうが破綻しにくい。

関数の合成で同じ構造を作る

TypeScriptJavaScript では、クラス階層を作らずに関数の合成で同じ構造を組むこともできる。固定したい手順を関数側に持ち、変わる部分を引数で受け取れば、継承なしでも「骨格は固定・ステップは差し替え」が成り立つ。省略できるフックはオプショナルなプロパティで表現する。

type ExportConfig<T> = {
  fetchData: () => Promise<T[]>;
  transform: (data: T[]) => string;
  save: (content: string) => Promise<void>;
  notify?: () => Promise<void>;
};

async function exportData<T>(config: ExportConfig<T>): Promise<void> {
  const data = await config.fetchData();
  const content = config.transform(data);
  await config.save(content);
  await config.notify?.();
}

// 使用
await exportData({
  fetchData: () => db.query('SELECT * FROM orders'),
  transform: (data) => JSON.stringify(data),
  save: async (content) => { await s3.putObject({ Bucket: 'exports', Key: 'orders.json', Body: content }); },
});

save の型は Promise<void> なので、SDK の戻り値をそのまま返すと型が合わない (putObject は ETag などを含むオブジェクトを返す)。await で受けて値を捨てる形にする。

実務での活用

  • Node.jsstream.Writable: 公式ドキュメントは _write() を「アプリケーションコードから直接呼び出してはならない。子クラスが実装し、内部の Writable のメソッドだけが呼び出す」と規定している (2026 年 8 月時点)。バッファリングや背圧を含む書き込みの流れは基底クラスが持ち、実装側は書き出し先だけを埋める。
  • テストフレームワークの beforeEach / afterEach: 前処理・本体・後処理を回す順序はランナー側が固定し、各フックの中身をテストの作者が書く。
  • クラスコンポーネント時代の React のライフサイクルメソッド: rendercomponentDidMount は呼ばれる側で、呼ぶタイミングは React が決める。ただし 2026 年 8 月時点の React 公式ドキュメントは Component をレガシー API に分類しており、新しく書くコードでは関数コンポーネントと Hooks を使う。

Express や Koa のミドルウェアチェーンもこのパターンの例に挙げられることがあるが、実際には各ミドルウェアが next() で処理を次へ渡す構造であり、Chain of Responsibility に近い。継承で手順の一部を埋める形ではないため、Template Method とは別の型として扱う。

実務での活用方法は関連書籍にも詳しい。

この記事は役に立ちましたか?

関連用語

関連する記事