テンプレートメソッドパターンは、処理の手順を親クラスに固定し、変わる部分だけをサブクラスに書かせるGoFの振る舞いパターンです。「穴あきの処理フロー」と考えると早いでしょう。この記事では、JDK 25とPython 3.14.6で実行したコードで構造を示し、Spring Boot 4.1.2で組むときに手順メソッドをfinalにすると@Transactionalが効かなくなる実測結果、SpringのOncePerRequestFilterやJdbcTemplateがこのパターンに当たるのかまで、公式ドキュメントの記述をもとに判断します。
まとめ:テンプレートメソッドパターンの要点
- 手順を固定するテンプレートメソッド、実装必須の抽象メソッド、既定実装を持つフックメソッドの3つで組み立てる
- Javaは
finalとabstractで手順の上書きも実装漏れもコンパイル時に止められる。Pythonの@finalは型チェッカー向けの表明で、実行時は素通りする - Spring Bootでは、
@Transactionalを付けたテンプレートメソッドをfinalにしない。CGLIBではそのメソッドにアドバイスを適用できず、本記事の検証構成ではトランザクションが開始されず、プロキシ側のフィールドもnullになった - Springの
OncePerRequestFilterは継承型のテンプレートメソッド、JdbcTemplateは可変部をコールバックで受け取る別の差し込み方 - 手順が本当に不変で、差分が1〜2箇所に収まるときだけ使う。可変部を実行時に切り替えたいならStrategyパターンを選ぶ
テンプレートメソッドパターンの構造:手順・抽象メソッド・フックの3役
3つの役割と呼び出しの向き
親クラスに、処理の順番だけを書いたメソッドを1本用意します。これがテンプレートメソッドです。その中から呼ばれる個々のステップのうち、サブクラスごとに必ず変わるものを抽象メソッドとして宣言し、たいていは共通だが時々変えたいものをフックメソッドとして既定実装つきで置きます。
| 役割 | 親クラスでの書き方 | サブクラスでの扱い |
|---|---|---|
| テンプレートメソッド | 共通の手順を実装 | 原則として上書きしない |
| 抽象メソッド | 宣言のみ | 実装が必須 |
| フックメソッド | 既定の実装つき | 必要なときだけ上書き |
呼び出しの向きに注目してください。利用側が呼ぶのは親クラスのテンプレートメソッドだけで、サブクラスのメソッドを呼ぶのは親クラスのほうです。サブクラスは「呼ばれるのを待つ部品」に徹します。手順の主導権を親クラス(あるいはフレームワーク)が握るこの構図は、制御の反転(IoC)の一例として説明されます。Springがこの考え方をコンテナ全体に広げた仕組みはIoCコンテナとは?制御の反転の仕組みとSpring IoCコンテナ・Beanのスコープとライフサイクルを動くコードで解説で扱っています。
設計で迷うのは抽象メソッドとフックメソッドの線引きです。「サブクラスが必ず答えを持っている問い」だけを抽象メソッドにします。既定値が決められる時点で、それは抽象メソッドではなくフックメソッドです。
GoF23パターンでの位置づけと「テンプレートパターン」という呼び名
出典はGamma・Helm・Johnson・Vlissidesによる書籍『Design Patterns: Elements of Reusable Object-Oriented Software』です。出版社Pearson(InformIT)の書誌ページによれば、同書がカタログ化したパターンは23種類で、生成・構造・振る舞いの3章に分かれています。テンプレートメソッドが属するのは振る舞いに関するパターンです。23種の全体像はGoFデザインパターン23種類の一覧|3分類と見分け方をJavaコードで解説にまとめています。
検索では「テンプレートパターン」「テンプレートメソッド」「Template Methodパターン」と表記が割れますが、どれも同じパターンです。テンプレートは「ひな形」の意味で、ここでは処理手順のひな形を親クラスが持つことを指します。単に「テンプレート」と書くとテンプレートエンジンやHTMLテンプレートと混同されるので、設計ドキュメントでは Template Method か「テンプレートメソッドパターン」まで書き切ってください。
Javaでの実装:finalによる手順の保護とabstractによる実装の強制
帳票の生成を例にします。「読み込む → 不正な行を除く → 出力形式に整える」という順番は変わらず、変わるのは読み込み方と出力形式です。順番をテンプレートメソッドに閉じ込め、読み込みと出力を抽象メソッドに、除外条件をフックメソッドにします。このJava例は単純なカンマ区切り入力を対象とし、引用符で囲まれたカンマやフィールド内改行を含むCSVの解析は扱いません。
import java.util.List;
import java.util.stream.Collectors;
abstract class ReportPipeline {
public final String run(String csv) { // テンプレートメソッド:手順を固定
List<String[]> rows = load(csv).stream().filter(this::isValid).toList();
return render(rows);
}
protected abstract List<String[]> load(String csv); // 抽象メソッド:実装必須
protected abstract String render(List<String[]> rows);
protected boolean isValid(String[] row) { // フックメソッド:既定の実装を持つ
return !row[0].isEmpty();
}
}
class CsvReport extends ReportPipeline {
@Override protected List<String[]> load(String csv) {
return csv.lines().skip(1).map(l -> l.split(",", -1)).toList();
}
@Override protected String render(List<String[]> rows) {
return rows.stream().map(r -> r[0]).collect(Collectors.joining(" / "));
}
}
class DevOnlyReport extends CsvReport {
@Override protected boolean isValid(String[] row) { // フックだけ差し替える
return row.length >= 2 && super.isValid(row) && row[1].equals("dev");
}
}
public class Main {
public static void main(String[] args) {
String src = "name,dept\nsato,dev\n,sales\nkato,sales\n";
System.out.println(new CsvReport().run(src));
System.out.println(new DevOnlyReport().run(src));
}
}
OpenJDK 25.0.4.1でjava Main.javaとして実行した結果です。
sato / kato
sato
DevOnlyReportが書いたのはisValidの3行だけで、読み込みも出力もCsvReportのものがそのまま動いています。フックの差し替えだけで振る舞いが変わるのが、このパターンの実利です。抽象メソッドをprotectedにしているのは、loadやrenderが部品であり外から単体で呼ばれるべきではないからです。公開するのはrunだけにすると、呼び出し側との契約が1本になります。
Javaでは骨組みの保護をコンパイラに任せられます。runを上書きするサブクラスと、抽象メソッドを実装し忘れたサブクラスをそれぞれjavacに通すと、どちらもコンパイルエラーで止まりました。
Bad.java:6: error: run() in Broken cannot override run() in Base
@Override public String run() { return "skip"; }
^
overridden method is final
Abs.java:2: error: Missing is not abstract and does not override abstract method step() in Base2
手順を守らせる強制力が言語機能で担保される。ここが、共通処理をコピーして揃える運用との決定的な差です。
Pythonでの実装:abc.ABCによる抽象化と@finalの実行時制約
Pythonには抽象メソッドを表す構文キーワードが無いため、標準ライブラリのabcモジュールを使います。ABCを継承し、実装必須のメソッドに@abstractmethodを付けます。
import csv, io
from abc import ABC, abstractmethod
from typing import final
class ReportPipeline(ABC):
@final
def run(self, csv_text): # テンプレートメソッド:手順を固定
rows = [r for r in self.load(csv_text) if self.is_valid(r)]
return self.render(rows)
@abstractmethod
def load(self, csv_text): ... # 抽象メソッド:実装必須
@abstractmethod
def render(self, rows): ...
def is_valid(self, row): # フックメソッド:既定の実装を持つ
return bool(row.get("name"))
class CsvReport(ReportPipeline):
def load(self, csv_text):
return list(csv.DictReader(io.StringIO(csv_text)))
def render(self, rows):
return " / ".join(r["name"] for r in rows)
class DevOnlyReport(CsvReport):
def is_valid(self, row): # フックだけ差し替える
return bool(row.get("name")) and row.get("dept") == "dev"
class Broken(CsvReport):
def run(self, csv_text): # @finalを無視して上書き
return "skipped"
src = "name,dept\nsato,dev\n,sales\nkato,sales\n"
print(CsvReport().run(src))
print(DevOnlyReport().run(src))
print(Broken().run(src))
ReportPipeline()
Python 3.14.6での実行結果です。
sato / kato
sato
skipped
TypeError: Can't instantiate abstract class ReportPipeline without an implementation for abstract methods 'load', 'render'
実装漏れは@abstractmethodがインスタンス化の時点でTypeErrorとして落とします。一方、@finalを付けたrunを上書きしたBrokenは、例外も出さず上書き後の実装で動きました。PEP 591は実行時に継承や上書きを禁止する案を検討したうえで「Nothing else in typing does any runtime enforcement, though, so final will not either.」と述べています。Pythonで上書きを検出するには、mypyなどの静的型チェックをCIに組み込む方法があります。実行時にも禁止したい場合は、別途その制約を実装する必要があります。
Spring Bootでテンプレートメソッドを組む:DIと抽象クラス
@ComponentによるサブクラスのBean登録とMapへの注入
Spring Bootでも組み方は素のJavaと同じで、抽象クラスに手順を書き、サブクラスを@ComponentとしてBean登録します。違いは利用側で、Map<String, ImportJob>を注入すると、同じ抽象型のBeanがBean名をキーにして全部渡されます。種類を増やすときはサブクラスを1つ足すだけで、利用側の分岐は増えません。次はSpring Boot 4.1.2(Spring Framework 7.0.10)とJDK 25で実行した検証アプリの抜粋です。検証ではspring-boot-starter-jdbcとH2を依存関係に追加し、自動構成されるDataSourceとトランザクションマネージャーを使用しています。以下ではimport文、起動クラス、メッセージを標準出力するAuditLogのBean定義を省略しています。publicクラスは別ファイルに置き、runnerメソッドは設定クラス内に定義します。saveは空実装なので、確認対象はDBへの保存ではなくトランザクションの有効状態です。
public abstract class ImportJob {
protected final AuditLog audit;
protected ImportJob(AuditLog audit) { this.audit = audit; }
@Transactional
public void execute(String src) { // テンプレートメソッド(finalにしない)
List<String> rows = parse(src);
rows.forEach(this::save);
audit.record(name() + " tx=" + TransactionSynchronizationManager.isActualTransactionActive()
+ " rows=" + rows.size());
}
protected abstract List<String> parse(String src);
protected abstract void save(String row);
protected String name() { return getClass().getSimpleName(); }
}
@Component
public class CsvImportJob extends ImportJob {
public CsvImportJob(AuditLog audit) { super(audit); }
@Override protected List<String> parse(String src) { return List.of(src.split(",")); }
@Override protected void save(String row) { }
}
// 利用側:同じ抽象型のBeanをBean名つきで受け取る
@Bean
CommandLineRunner runner(Map<String, ImportJob> jobs) {
return args -> {
System.out.println("beans=" + jobs.keySet());
jobs.get("csvImportJob").execute("a,b,c");
};
}
beans=[csvImportJob]
audit: CsvImportJob tx=true rows=3
抽象クラスに付けた@TransactionalはサブクラスのBeanにも効き、手順全体が1つのトランザクションで動いています(tx=true)。コンストラクタ注入したAuditLogもサブクラス経由で親クラスに渡っています。
CGLIBプロキシにおけるfinalの制約とNPEの再現条件
今回のようにCGLIBプロキシ経由でトランザクションを適用する構成では、対象の手順メソッドをfinalにすると、そのメソッドにアドバイスを適用できません。@Transactional public final void execute(...)を持つBeanを同じ環境で起動すると、まず起動時に次の警告が出ました。
WARN ... o.s.aop.framework.CglibAopProxy : Public final method [public final void demo.FinalJob.execute(java.lang.String)] cannot get proxied via CGLIB, consider removing the final marker or using interface-based JDK proxies.
そのままexecuteを呼ぶと、トランザクションは張られず、コンストラクタで注入したはずのフィールドがnullでした。
final: class=FinalJob$$SpringCGLIB$$0 audit=null tx=false
java.lang.NullPointerException: Cannot invoke "demo.AuditLog.record(String)" because "this.audit" is null
理由はSpringのプロキシの作り方にあります。Spring Frameworkリファレンスのプロキシの章は、インタフェースを実装しないBeanには対象クラスのサブクラスとしてCGLIBプロキシを作るとし、その制約に「final methods cannot be advised, because they cannot be overridden.」を挙げています。Spring Bootではspring.aop.proxy-target-classの既定値がtrue(4.1.2の構成メタデータで確認)なので、インタフェースを実装したBeanでもサブクラス方式のCGLIBプロキシが使われます。さらに同じ箇所に、CGLIBプロキシのインスタンスはObjenesisで作られコンストラクタが呼ばれないとあります。finalメソッドはプロキシが上書きできないため、呼び出しは本物のBeanへ転送されず、コンストラクタを通っていないプロキシ自身の上で実行される。だからauditがnullになります。
対処は2つです。@Transactionalなどアノテーションで振る舞いを足すメソッドはfinalにせず、上書きされないことはコードレビューと命名で担保する。どうしてもfinalで守りたいなら、トランザクション境界をテンプレートメソッドの外(呼び出し側のサービス)に移し、テンプレートを持つクラスにはアスペクトが一切かからないようにします。
ステップの自己呼び出しとプロキシ型AOPの制約
テンプレートメソッドは、各ステップをthis経由で呼びます。Springのプロキシはこの自己呼び出しに割り込めません。リファレンスの同じ章は「self invocation via an explicit or implicit this reference will bypass the advice.」と明記しており、実際に手順メソッドから@Transactional付きのステップを呼んだところ、ステップ内のトランザクションは無効(tx=false)でした。
「保存ステップだけREQUIRES_NEWで別トランザクションにしたい」といった要求は、ステップのメソッドにアノテーションを付けても実現できません。そのステップを別のBeanに切り出して注入するか、トランザクションをテンプレートメソッド単位で設計します。自己呼び出しとプロキシの関係はAOP(アスペクト指向プログラミング)とは?Spring Bootでの使い方・実行順序と自己呼び出しの落とし穴でも詳しく扱っています。
フレームワークの実例:OncePerRequestFilter・InputStream・unittest
OncePerRequestFilter:Servletフィルターにおける手順と拡張点
Spring Bootで認証やログ用のフィルターを書くとき継承するOncePerRequestFilterは、継承型のテンプレートメソッドそのものです。Spring Framework 7.0.10のソースコードでは、doFilterが手順を持ちます。HTTPリクエストかを確かめ、shouldNotFilterがtrueなら素通しし、同じリクエストで処理済みの印があれば二重実行を避け、印を付けてからdoFilterInternalを呼び、最後に印を外す。開発者が書くのはprotected abstractのdoFilterInternalだけで、除外条件を足したいときに既定でfalseを返すフックのshouldNotFilterを上書きします。
ひとつ注意があります。7.0.10のdoFilterにはfinalが付いていません(手元の6.2.17〜7.0.10のjarをjavapで確認)。上書きは言語上は可能ですが、既定の処理を置き換えると重複実行を防ぐ仕組みを損なうおそれがあります。また、実行回数の保証にはディスパッチとスレッドの条件があり、非同期ディスパッチなどを含むリクエスト全体で常に1回とは限りません。サブクラスではdoFilterに触れず、doFilterInternalとshouldNotFilterだけを書いてください。
java.io.InputStream:read()を1本書けば動く代償
Javaのjava.io.InputStreamも典型例です。Java SE 25のJavadocによれば、引数なしのread()が抽象メソッドで、サブクラスはこれだけを実装すればread(byte[], int, int)なども含めてストリームとして機能します。
代償も同じJavadocに書かれています。read(byte[] b, int off, int len)の説明は「The read(b, off, len) method for class InputStream simply calls the method read() repeatedly.」で、続けて「Subclasses are encouraged to provide a more efficient implementation of this method.」と促しています。read()だけを実装すると、配列への読み込みでも単一バイトの読み込みが繰り返されます。ただし、そのたびにシステムコールやネットワーク往復が発生するかは、サブクラスと下位層の実装によります。まとめ読みできるソースを扱うならread(byte[], int, int)もオーバーライドし、利用側ではBufferedInputStreamで包みます。「1本実装すれば動く」手軽さの裏で、性能の責任はサブクラスの設計者に残ります。
unittest.TestCase:setUpとtearDownはフックメソッド
Pythonのunittestでは、テストの実行手順をTestCase.run()が固定し、前処理のsetUpと後処理のtearDownが何もしない既定実装を持つフックとして開放されています。公式ドキュメントはtearDownについて「If setUp() succeeded, tearDown() will be run whether the test method succeeded or not.」と説明しています。裏返せば、setUpが例外を投げるとtearDownは呼ばれません。setUpで複数のリソースを順に確保するなら、確保のたびにaddCleanupで解放処理を登録しておくと取り残しを防げます。フックは「必ず呼ばれる」とは限らず、いつ呼ばれるかはテンプレートメソッド側の仕様で決まります。
JdbcTemplateとの違い:コールバックによる可変部の受け渡し
クラス名にTemplateと付くため、SpringのJdbcTemplateをテンプレートメソッドパターンの実例として挙げる解説が目立ちます。公式ドキュメントの記述に照らすと、この対応づけは厳密ではありません。
Spring Frameworkリファレンスのデータアクセス章は、JdbcTemplateが「handles the creation and release of resources」し、「performs the basic tasks of the core JDBC workflow (such as statement creation and execution), leaving application code to provide SQL and extract results」と説明します。固定手順をフレームワークが持ち、可変部をアプリケーションが埋める。ここまでは同じ発想です。
違うのは埋め方です。同じ章は「you need only to implement callback interfaces, giving them a clearly defined contract」と述べ、PreparedStatementCreatorやRowCallbackHandlerといったコールバックインタフェースを挙げています。可変部を担うのはJdbcTemplateを継承したサブクラスではなく、引数として渡されるオブジェクト(ラムダ)です。GoFのテンプレートメソッドは可変部をサブクラスに委ねる形なので、委譲で埋めるJdbcTemplateは厳密には当てはまりません。
| クラス | 可変部の受け取り方 | GoFのテンプレートメソッドか |
|---|---|---|
| OncePerRequestFilter | 継承してdoFilterInternalを実装 | 該当する |
| AbstractRoutingDataSource | 継承してdetermineCurrentLookupKeyを実装 | 該当する |
| JdbcTemplate | コールバックを引数で渡す | 発想は同じ・差し込み方が違う |
クラス名にTemplateとあるかどうかでは判断できません。拡張点が「継承して埋める抽象メソッド」か「引数で渡すコールバック」かを見てください。この違いは、次のStrategyとの使い分けにそのまま効いてきます。
Strategy・Factory Methodとの関係:拡張方式の比較と生成処理での併用
Strategyとの使い分けは可変点の数と切り替えのタイミング
| 観点 | テンプレートメソッド | Strategy |
|---|---|---|
| 可変部の埋め方 | 継承・オーバーライド | 委譲・オブジェクト注入 |
| 切り替えのタイミング | クラス定義時 | 実行時に差し替え可 |
| 組み合わせ数 | 可変点の数だけ乗算 | 可変点ごとに独立 |
| 手順の持ち主 | 親クラスに1本 | 呼び出し側 |
可変点が1つで、処理の順番そのものを共有したいならテンプレートメソッドが簡潔です。可変点が複数あり、それぞれを独立して組み合わせたい場合は、委譲による構成を優先します。読み込み形式3種と出力形式3種の全組み合わせを専用の具象サブクラスで表すなら9クラスになります。委譲で各形式を独立した部品にすれば、可変部の実装は3+3の6クラスに分けられます。ここでは共通の親クラス、インタフェース、利用側のクラスを数えていません。委譲側の書き方はStrategyパターンとは?if文の分岐を置き換える仕組みとPHP・Java・Python・TypeScriptの実装例に実装例があります。
生成ステップにおけるFactory Methodとの併用
テンプレートメソッドの抽象メソッドのうち、「オブジェクトを生成して返す」ステップがFactory Methodです。たとえば手順の途中でcreateParser()を呼び、どのパーサーを作るかはサブクラスが決める形にすると、テンプレートメソッドの中にFactory Methodが入ります。2つのパターンは競合するものではなく、テンプレートメソッドが定めた手順の中で、生成するオブジェクトの選択をFactory Methodに任せられます。生成の分離そのものはファクトリパターンとは?Factory Method・Abstract Factoryの違いとJava・Python実装例で解説しています。
テンプレートメソッドパターンのデメリットと採用を見送るべき場面
このパターンは継承を前提にしており、継承の弱点をそのまま引き継ぎます。次のいずれかに当てはまるなら採用しないでください。
手順そのものが変わる可能性がある場合。固定されるのはステップの順番です。「あるサブクラスだけ検証を2回走らせたい」「別のサブクラスでは整形を飛ばしたい」という要求が出た時点で、親クラスに条件分岐とフックが増えていきます。フックを足し続けた親クラスは、どのサブクラスがどのフックを上書きしているのか追えなくなります。
可変点が3つ以上ある場合。3つの可変点にそれぞれ3つの選択肢があり、全組み合わせを専用の具象サブクラスで表すなら27クラスになります。迷わずStrategyやファクトリによる生成の分離に寄せてください。
親クラスを自分たちで保守できない場合。外部ライブラリの抽象クラスを継承する場合、公開された拡張契約を超えて内部の処理順や呼び出し回数に依存すると、更新時に互換性を失うおそれがあります。フックが呼ばれる順番や回数は公開APIとして明示されていないことが多く、マイナーバージョンアップで変わっても仕様違反とは言えません。外部の抽象クラスに依存するなら、自分たちのコードとの間にアダプタを1枚挟みます。
Springで手順メソッドにAOPのアノテーションを付けたい場合。前述のとおりfinalで手順を守る定石とプロキシが衝突し、ステップ単位のアノテーションも自己呼び出しで無効になります。ステップごとにトランザクションや非同期実行を変えたいなら、ステップを別Beanに分けた委譲の構成のほうが素直です。
手順が仕様として確定し、可変部の独立した組み合わせが不要で、親クラスを自分たちで保守できるなら、テンプレートメソッドは少ない構成要素で共通処理を集約できる選択肢です。継承1段で済み、追加のインタフェース定義も注入の配線も要りません。既存システムの共通処理をこうした設計で整理し直す案件は、フルスクラッチ開発の支援でも扱っています。
よくある質問
テンプレートメソッドパターンのメリットは?
共通の処理手順を親クラス1か所に集約できるため、手順の修正がすべてのサブクラスへ一度に反映され、サブクラスは差分だけを書けば済みます。Javaならfinalで手順の上書きを、abstractで実装漏れをコンパイル時に止められ、Pythonでも@abstractmethodの未実装はインスタンス化の時点でTypeErrorになります。
Spring Bootでテンプレートメソッドパターンは使えますか?
使えます。抽象クラスに手順を書き、サブクラスを@Componentで登録し、利用側ではMap<String, 抽象型>やList<抽象型>で注入します。ただし@Transactionalなどを付けたテンプレートメソッドをfinalにすると、CGLIBプロキシが割り込めず、そのメソッドのアノテーションによるトランザクション開始が行われません。本記事のSpring Boot 4.1.2による検証構成では、プロキシ側のフィールドもnullになりました。手順の中からthis経由で呼ぶステップに付けた@Transactionalなども、自己呼び出しのためプロキシを通らず効きません。
SpringのJdbcTemplateはテンプレートメソッドパターンですか?
厳密には異なります。Spring Frameworkリファレンスは、JdbcTemplateが定型のJDBC処理を担い、アプリケーション側はRowCallbackHandlerなどのコールバックインタフェースを実装すればよいと説明しています。可変部を渡すのは継承ではなく引数(委譲)です。Springで継承型のテンプレートメソッドに当たるのはOncePerRequestFilterやAbstractRoutingDataSourceです。
Strategyパターンとの違いは?
可変部の差し込み方が違います。テンプレートメソッドはサブクラスのオーバーライドで埋めるため、組み合わせはクラス定義時に決まります。Strategyは振る舞いをオブジェクトとして外から渡すため、実行時に差し替えられます。可変点が1つで手順を共有したいならテンプレートメソッド、可変点が複数あるか実行時に切り替えたいならStrategyを選んでください。
デザインパターンは全部で何個ありますか?
GoFと呼ばれるGamma・Helm・Johnson・Vlissidesの書籍『Design Patterns: Elements of Reusable Object-Oriented Software』がカタログ化したのは23パターンで、出版社Pearson(InformIT)の書誌ページにも23と記載されています。生成・構造・振る舞いの3分類で、テンプレートメソッドは振る舞いに関するパターンに含まれます。