概要

処理の全体的な流れを親クラスで固定し、個々のステップだけをサブクラスでカスタマイズする。Template Method パターンは、バッチ処理、データ移行、ETL パイプラインなど、手順は共通だが各ステップの中身が異なる場面で威力を発揮します。テンプレートメソッドを final にすることで、処理の順序を強制しつつ、abstract メソッドで必須のカスタマイズポイントを定義し、フックメソッドでオプションの拡張ポイントを提供します。この記事では、データ移行バッチ(読み込み → 処理 → 書き込み → 後処理)を題材に実装し、record でバッチ結果を返す方法、Java 21 の sealed interface で各ステップを型安全に定義する方法を確認します。

使いどころ

データ移行バッチの共通フロー(接続 → データ読込 → 変換 → 書込 → 後処理)を親クラスに定義し、移行元ごとにサブクラスでステップを実装する

帳票出力処理のヘッダー → 明細 → フッターの共通フローを固定し、帳票種別ごとに出力内容をカスタマイズする

テストの共通セットアップ → テスト実行 → 検証 → クリーンアップのフローを基底クラスに定義し、テストケースごとに内容を変える

コード例

TemplateMethodDemo.java
public class TemplateMethodDemo {

    record BatchResult(String batchName, int recordCount,
                       boolean success) {}

    static abstract class DataMigrationBatch {
        private final String batchName;
        public DataMigrationBatch(String name) {
            this.batchName = name;
        }

        public final BatchResult execute() {
            System.out.println("=== " + batchName + " 開始 ===");
            readData();
            processData();
            writeData();
            cleanup();
            System.out.println("=== " + batchName + " 終了 ===");
            return new BatchResult(batchName,
                                   getRecordCount(), true);
        }

        protected abstract void readData();
        protected abstract void processData();
        protected abstract void writeData();
        protected int getRecordCount() { return 0; }
        protected void cleanup() {
            System.out.println("[後処理] デフォルト");
        }
    }

    static class UserMigrationBatch extends DataMigrationBatch {
        public UserMigrationBatch() { super("ユーザー移行"); }
        @Override protected void readData() {
            System.out.println("[読込] 旧DBから1000件");
        }
        @Override protected void processData() {
            System.out.println("[処理] メール正規化");
        }
        @Override protected void writeData() {
            System.out.println("[書込] 新DBに1000件");
        }
        @Override protected int getRecordCount() {
            return 1000;
        }
    }

    static class OrderMigrationBatch extends DataMigrationBatch {
        public OrderMigrationBatch() { super("注文移行"); }
        @Override protected void readData() {
            System.out.println("[読込] CSVから5000件");
        }
        @Override protected void processData() {
            System.out.println("[処理] 金額再計算");
        }
        @Override protected void writeData() {
            System.out.println("[書込] 新DBに5000件");
        }
        @Override protected int getRecordCount() {
            return 5000;
        }
        @Override protected void cleanup() {
            System.out.println("[後処理] CSVアーカイブ");
        }
    }

    public static void main(String[] args) {
        var userResult = new UserMigrationBatch().execute();
        System.out.println("結果: " + userResult);

        System.out.println();

        var orderResult = new OrderMigrationBatch().execute();
        System.out.println("結果: " + orderResult);
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record で BatchResult を定義し、テンプレートメソッドから処理結果をまとめて返せる。var で変数宣言も簡潔に。

Java 17
// Java 17: record で処理結果をまとめて返す
record BatchResult(String batchName, int recordCount,
                   boolean success) {}

public final BatchResult execute() {
    readData();
    processData();
    writeData();
    cleanup();
    var result = new BatchResult(
        batchName, getRecordCount(), true);
    return result;
}

Library Comparison

標準 API(abstract class + final メソッド)処理の骨格が明確で、ステップのカスタマイズポイントが限られている場面。外部依存なしで完結する。継承ベースのため、Javaの単一継承制約を消費する。Strategy パターンとの併用も検討する。
Strategy パターンとの組み合わせ処理ステップを継承ではなく委譲で差し替えたい場合。テスト容易性が高い。Strategy を注入する仕組みが必要になり、設計がやや複雑になる。
Spring Batch(Step / Tasklet)大規模なバッチ処理でジョブ定義・リスタート・チャンク処理を統一的に扱う場合。Spring 依存で学習コストが高い。小規模バッチには過剰。

注意点

テンプレートメソッドは final にして、サブクラスが処理の順序を変えられないようにすること。final がないと意図しないオーバーライドで処理フローが壊れる

フックメソッドのデフォルト実装が空の場合は、サブクラスがオーバーライドしなくても動く設計にすること。必須のカスタマイズポイントは abstract にする

サブクラスが増えすぎると継承階層が深くなる。Strategy パターンとの併用(処理ステップを Strategy で差し替え)も検討する

テンプレートメソッド内で例外が発生した場合のクリーンアップ処理を確実に行うこと。try-finally か try-with-resources を検討する

FAQ

Template Method と Strategy はどう使い分けますか。

Template Method は処理の骨格を固定して部分だけカスタマイズします。Strategy はアルゴリズム全体を差し替えます。骨格が共通なら Template Method、全く異なる処理なら Strategy が適しています。

フックメソッドとは何ですか。

デフォルト実装を持つメソッドで、サブクラスが必要に応じてオーバーライドします。abstract メソッドと異なり、オーバーライドは任意です。

テンプレートメソッドを final にしない場合のリスクは何ですか。

サブクラスが処理の順序を変更できてしまいます。readData の前に writeData が呼ばれるなど、意図しない動作の原因になります。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます