java-recipes

ホーム デザインパターン › DP-22

DP-22: Template Method パターン

アルゴリズムの「骨組み(流れ)」をスーパークラスで定義し、具体的な処理をサブクラスに委ねるパターンです。 バッチ処理やデータ移行など「手順が共通で中身だけ変わる」処理に最適です。

Template Method パターンとは

複数のバッチ処理を作るとき、「データ読み込み → データ処理 → データ書き込み → 後処理」という手順は どのバッチでも共通です。しかし「何を読み込むか」「どう処理するか」はバッチごとに異なります。 Template Method パターンでは、この「共通の手順(テンプレート)」をスーパークラスの テンプレートメソッドとして定義し、「変わる部分」だけをサブクラスに実装させます。

final キーワードの役割

テンプレートメソッド(execute())にはfinal キーワードをつけるのが慣習です。final をつけることで 「処理の順序はスーパークラスが管理する」ことが明確になり、 サブクラスが誤って手順を変えてしまうことを防げます。

// final をつけることでサブクラスでのオーバーライドを禁止
public final void execute() {
    readData();    // ← ここの順序は変えられない
    processData();
    writeData();
    cleanup();
}

Java 標準ライブラリや Java EE / Jakarta EE でも Template Method パターンは広く使われています。

Java 標準ライブラリの Template Method 実例

// 例1: javax.servlet.http.HttpServlet
// service() がテンプレートメソッド(HTTP メソッドで振り分ける)
// doGet() / doPost() をオーバーライドして具体的な処理を実装する
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        // GET リクエストの処理を実装
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
        // POST リクエストの処理を実装
    }
}

// 例2: JUnit 4 のテストライフサイクル
// テストフレームワークが setUp() → テストメソッド → tearDown() の順で呼ぶ
public class MyTest extends TestCase {
    @Override
    protected void setUp() {
        // テスト前の準備(DBコネクション作成など)
    }
    @Override
    protected void tearDown() {
        // テスト後の後処理(DBコネクションのクローズなど)
    }
}

サンプルコード

TemplateMethodSample.java
public class TemplateMethodSample {

    // バッチ処理の骨組みを定義する抽象クラス
    static abstract class DataMigrationBatch {
        private String batchName;

        public DataMigrationBatch(String batchName) {
            this.batchName = batchName;
        }

        // テンプレートメソッド: 処理の順序を定義する(final でオーバーライドを禁止)
        public final void execute() {
            System.out.println("=== " + batchName + " 開始 ===");
            readData();      // データ読み込み(サブクラスで実装)
            processData();   // データ処理(サブクラスで実装)
            writeData();     // データ書き込み(サブクラスで実装)
            cleanup();       // 後処理(デフォルト実装あり、オーバーライド可能)
            System.out.println("=== " + batchName + " 終了 ===");
        }

        // サブクラスが必ず実装しなければならない抽象メソッド
        protected abstract void readData();
        protected abstract void processData();
        protected abstract void writeData();

        // フックメソッド: デフォルト実装を持ち、必要に応じてオーバーライドできる
        protected void cleanup() {
            System.out.println("[後処理] 一時ファイルを削除しました(デフォルト)");
        }

        public String getBatchName() {
            return batchName;
        }
    }

    // ユーザーデータ移行バッチ: DataMigrationBatch を具体化したサブクラス
    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件書き込みました");
        }

        // cleanup はオーバーライドしない(デフォルト実装を利用する)
    }

    // 注文データ移行バッチ: DataMigrationBatch を具体化したサブクラス
    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件書き込みました");
        }

        // cleanup をオーバーライドして追加処理を行う
        @Override
        protected void cleanup() {
            System.out.println("[後処理] 処理済みCSVファイルをアーカイブしました");
            System.out.println("[後処理] 処理完了メールを管理者に送信しました");
        }
    }

    public static void main(String[] args) {
        System.out.println("=== Template Method パターン: バッチ処理 ===");
        System.out.println();

        // ユーザーデータ移行バッチを実行
        DataMigrationBatch userBatch = new UserMigrationBatch();
        userBatch.execute();

        System.out.println();

        // 注文データ移行バッチを実行
        DataMigrationBatch orderBatch = new OrderMigrationBatch();
        orderBatch.execute();
    }
}

Java 8 ではテンプレートメソッド(execute())に final をつけることで「処理の順序はスーパークラスが管理し、サブクラスは変更できない」ことを明示します。cleanup() はフックメソッドとしてデフォルト実装を持ち、必要なサブクラスだけオーバーライドします。

よくあるミス・注意点

⚠️ 抽象メソッドを増やしすぎてサブクラスの実装が大変になる

抽象メソッドが多すぎると、サブクラスを作るたびに全てのメソッドを実装しなければなりません。 「必須でない処理」は抽象メソッドではなく、空実装またはデフォルト処理を持つ 「フックメソッド(hook method)」として提供しましょう。 サンプルコードの cleanup() がフックメソッドの例です。

⚠️ テンプレートメソッドに final をつけ忘れる

final をつけ忘れると、 サブクラスが execute() をオーバーライドして 処理の順序を変えてしまう可能性があります。 Template Method パターンの意図は「手順はスーパークラスが守る」ことなので、 テンプレートメソッドには必ず final をつけましょう。

⚠️ サブクラス側で super.execute() を呼んでしまう

サブクラスで execute() をオーバーライドしてsuper.execute() を呼ぶと、 処理が二重に実行される場合があります。 テンプレートメソッドは呼び出す側(利用者)が直接呼ぶもので、 サブクラスからは抽象メソッド・フックメソッドだけをオーバーライドするのが正しい使い方です。

テストする観点

  • テンプレートメソッドが readData()processData()writeData()cleanup() の順に呼び出されること
  • デフォルトの cleanup()(フックメソッド)が正しく動作すること
  • サブクラスが cleanup() をオーバーライドした場合に、オーバーライド後の実装が呼ばれること
  • Java 17 版で execute() が正しい BatchResult(batchName・recordCount・success)を返すこと
  • 各サブクラス(UserMigrationBatch・OrderMigrationBatch)が期待通りのログを出力すること
  • テンプレートメソッドが final であり、サブクラスでオーバーライドできないこと(コンパイルエラーで確認)

GitHub でソースコードを見る →