コーディング · デザインパターン
Template Method パターンとは?例でわかりやすく解説
結論
Template Method は振る舞いに関するデザインパターンで、アルゴリズムの骨組みを基底クラスのメソッドに定義し、全体の順序を変えずに、特定の手順をサブクラスがオーバーライドできるようにします。基底クラスは何がいつ起きるかを決め、サブクラスは一部の手順をどう行うかを決めます。デフォルトの実装を持つ任意の手順を、フックと呼びます。
目的と解決する問題
目的:アルゴリズムの骨組みを 1 つの操作として定義し、いくつかの手順をサブクラスに任せることです。Template Method は、アルゴリズムの構造を変えずに、サブクラスがアルゴリズムの特定の手順を再定義できるようにします。Gang of Four(GoF)の書籍にある振る舞いパターンの 1 つです。
データを CSV と JSON で出力するレポートツールを考えてみましょう。どちらのエクスポーターも、同じ順序で同じことをします。行を読み込み、行があるかを確認し、整形し、行数を示すフッターを加えます。違うのは整形だけです。それぞれのエクスポーターを別々に書くと、共通の手順がコピーされ、一方での修正(たとえば、空の結果の扱いの改善)が、もう一方で忘れられがちです。
Template Method では、基底クラス Exporter が、手順の順序を固定した export メソッドを持ちます。変わる手順は抽象メソッドです。CsvExporter と JsonExporter は、それらだけを実装します。
構造と登場する役割
| 役割 | 責務 |
|---|---|
| Abstract class | 手順を固定された順序で呼び出す Template method を実装し、抽象的な手順を宣言し、フックを提供することもある |
| Template method | アルゴリズムを定義する公開メソッド。通常はオーバーライドされることを想定していない |
| Primitive operation | すべてのサブクラスが実装しなければならない抽象的な手順 |
| Hook | デフォルトの(多くは空の)実装を持つ手順。サブクラスはオーバーライドしてもよい |
| Concrete class | Primitive operation を実装し、必要に応じてフックをオーバーライドする |
この設計は制御を逆転させます。基底クラスがサブクラスのメソッドを呼び、その逆ではありません。これはハリウッドの原則「こちらから連絡するので、電話しないでください」と呼ばれることもあります。
TypeScript での実装
export が Template method です。format はサブクラスが実装しなければならない Primitive operation で、header は空のデフォルトを持つフックです。
type Row = { name: string; score: number };
abstract class Exporter {
// The template method: fixed order of steps
export(rows: Row[]): string {
if (rows.length === 0) return "no data";
const lines = [this.header(), this.format(rows), "rows: " + rows.length];
return lines.filter((line) => line !== "").join("\n");
}
protected abstract format(rows: Row[]): string; // required step
protected header(): string { return ""; } // hook: optional step
}
class CsvExporter extends Exporter {
protected header() { return "name,score"; }
protected format(rows: Row[]) {
return rows.map((r) => r.name + "," + r.score).join("\n");
}
}
class JsonExporter extends Exporter {
protected format(rows: Row[]) {
return JSON.stringify(rows);
}
}
const rows = [{ name: "Ana", score: 9 }, { name: "Ben", score: 7 }];
console.log(new CsvExporter().export(rows));
// name,score
// Ana,9
// Ben,7
// rows: 2
console.log(new JsonExporter().export(rows));
// [{"name":"Ana","score":9},{"name":"Ben","score":7}]
// rows: 2
console.log(new JsonExporter().export([])); // no dataデータが空かどうかのチェックとフッターは、1 か所にあります。新しい XML エクスポーターは、format を実装するだけで、残りを自動的に手に入れます。
合成による同じ考え方
継承だけが方法ではありません。変わる手順を関数として渡せば、このパターンは Strategy に近いものになり、クラス階層を避けられます。
type Row = { name: string; score: number };
type Steps = { format: (rows: Row[]) => string; header?: () => string };
function exportRows(rows: Row[], steps: Steps): string {
if (rows.length === 0) return "no data";
const lines = [steps.header?.() ?? "", steps.format(rows), "rows: " + rows.length];
return lines.filter((line) => line !== "").join("\n");
}
console.log(exportRows([{ name: "Ana", score: 9 }], { format: (r) => r[0].name }));
// Ana
// rows: 1手順が小さく、基底クラスの共有状態が不要な場合や、1 つのエクスポーターが複数の出どころから手順を組み合わせたい場合は、この形のほうが適しています。
使うべき場面と避けるべき場面
- 複数のクラスが同じアルゴリズムを実行し、違うのがいくつかの手順だけで、重複する部分をなくしたいときに使います。
- テストの setup と teardown のように、操作の順序を制御しつつ、利用者に手順をカスタマイズさせたいフレームワークで使います。
- すべてのサブクラスに実装を強制せずに、任意の拡張ポイントを提供するために、フックを使います。
- バリエーションが大きい場合や、実行時に変える必要がある場合は使いません。合成を使う Strategy のほうが柔軟です。
- Template Method を何段にも積み重ねた階層は避けましょう。サブクラスのサブクラスのさらにサブクラスが手順をオーバーライドすると、流れを追うのが難しくなります。
実際の利用例
- Java の
AbstractList:get(int)とsize()を実装すると、基底クラスが、それらを基にした反復処理、indexOf、containsなどのメソッドを提供します。 - Java の
InputStream:サブクラスが 1 バイトのread()を実装し、基底クラスがそれを呼ぶことでread(byte[], int, int)を実装します(速度のためにサブクラスがオーバーライドすることもあります)。 - テストフレームワーク:JUnit、Python の
unittestなどは、setUp、テストメソッド、tearDownを決まった順序で呼び、必要な手順だけをオーバーライドします。 - React のクラスコンポーネントの
componentDidMountのようなライフサイクルメソッドを持つ UI フレームワークは、自分のアルゴリズムの決まったタイミングで、あなたのメソッドを呼びます。
関連するパターン
| パターン | Template Method パターンとの違い |
|---|---|
| Strategy パターン | Strategy は合成によって、実行時にアルゴリズム全体を置き換えます。Template Method はアルゴリズムを固定し、コンパイル時に継承によってその一部を変えます。 |
| Factory Method パターン | Factory Method は、Template Method の手順の 1 つ、つまりオブジェクトを生成する手順であることがよくあります。 |
| フック関数とコールバック | 固定されたアルゴリズムにコールバックを渡すのは、同じ考え方を合成で実現したものです。 |
よくある質問
Template Method パターンのフックとは何ですか?
フックは、基底クラスにデフォルトの実装を持つ手順で、何もしないことも多いです。サブクラスはオーバーライドしてその時点で動作を追加できますが、必須ではありません。
Template Method と Strategy の違いは何ですか?
Template Method は継承を使います。基底クラスがアルゴリズムを持ち、サブクラスが手順をオーバーライドします。Strategy は合成を使います。Context が、アルゴリズム全体を、実行時に差し替えられるオブジェクトや関数に委譲します。
Template method 自体はオーバーライドできるべきですか?
いいえ。その目的は手順の順序を固定することです。Java や C# では final や非 virtual にできます。TypeScript には final キーワードがないので、ドキュメントに書く必要があります。
Template Method と Factory Method はどんな関係ですか?
Factory Method は、Template Method の特殊化であることがよくあります。基底クラスのアルゴリズムにオブジェクトを生成する手順が含まれていて、どのクラスを生成するかをサブクラスが決めます。