Engineering
Glossvation Engineering
アルゴリズム

TemplateMethod パターン — Spring JdbcTemplate に見る典型実装

処理の骨格は親、差分は子に書き分ける Template Method。Spring の `*Template` 系の設計思想と、Strategy / Decorator との使い分け。

Glossvation ·

TemplateMethod パターン概念図

どういう問題を解くパターンか

処理の 「骨格」 (呼び出し順序や共通処理) は親クラスに固定し、 「差分」 (個別のビジネスロジック) だけサブクラスに書かせる パターン。

典型例:

  • ファイル読み込み: open → read → close の順序は固定、read() の中身だけが型ごとに違う
  • トランザクション: 開始 → 業務処理 → コミット/ロールバックの枠は固定、業務処理だけ差分
  • バッチジョブ: 事前チェック → 本処理 → 事後処理の順序は固定、本処理だけ差分

骨格を親クラスに final で書く のが Template Method の肝。 子クラスから順序を壊させません。

いつ使う / 使わない

使うべき使わないほうがよい
処理順序が 絶対に固定順序も含めて差し替えたい
差分が常に 1 ヶ所に閉じている差分が多方向に広がる
ユースケースが継承で素直に表現できるコンポジションの方が自然

Strategy との違い: Strategy は 「アルゴリズム全体を差し替える」、Template Method は 「骨格の一部だけを差し替える」。 たとえば データ出力の順序 (ヘッダ→ボディ→フッタ) は固定で、ボディの作り方だけ差し替えたい なら Template Method。 出力形式ごと丸ごと切り替えたい (CSV と JSON を切り替え) なら Strategy。

実装

抽象クラス

public abstract class ReportExporter {

    /** 骨格メソッド (final で順序を保護) */
    public final void export(OutputStream out) throws IOException {
        openHeader(out);
        writeBody(out);
        closeFooter(out);
    }

    protected abstract void openHeader(OutputStream out) throws IOException;
    protected abstract void writeBody(OutputStream out) throws IOException;
    protected abstract void closeFooter(OutputStream out) throws IOException;
}

具象クラス

public class CsvReportExporter extends ReportExporter {
    private final List<Row> rows;

    public CsvReportExporter(List<Row> rows) {
        this.rows = rows;
    }

    @Override
    protected void openHeader(OutputStream out) throws IOException {
        out.write("id,name,amount\n".getBytes(StandardCharsets.UTF_8));
    }

    @Override
    protected void writeBody(OutputStream out) throws IOException {
        for (Row r : rows) {
            String line = r.id() + "," + r.name() + "," + r.amount() + "\n";
            out.write(line.getBytes(StandardCharsets.UTF_8));
        }
    }

    @Override
    protected void closeFooter(OutputStream out) throws IOException {
        // CSV は特に何もしない
    }
}

public class JsonReportExporter extends ReportExporter {
    private final List<Row> rows;

    public JsonReportExporter(List<Row> rows) {
        this.rows = rows;
    }

    @Override protected void openHeader(OutputStream out) throws IOException {
        out.write("{\"items\":[".getBytes(StandardCharsets.UTF_8));
    }
    @Override protected void writeBody(OutputStream out) throws IOException {
        // ... items を JSON で書き出す
    }
    @Override protected void closeFooter(OutputStream out) throws IOException {
        out.write("]}".getBytes(StandardCharsets.UTF_8));
    }
}

呼び出し側

ReportExporter exporter = new CsvReportExporter(rows);
exporter.export(outputStream);

骨格 (openHeader → writeBody → closeFooter の順序) はクライアント側では意識不要です。

Spring JdbcTemplate が完璧な実例

Spring の JdbcTemplate は Template Method の教科書的な実装です。

// Spring 側 (擬似的な骨格)
public <T> T execute(ConnectionCallback<T> callback) {
    Connection con = dataSource.getConnection();  // 骨格: 接続取得
    try {
        return callback.doInConnection(con);      // 差分: ユーザーロジック
    } catch (SQLException e) {
        throw translateException(e);               // 骨格: 例外変換
    } finally {
        releaseConnection(con);                    // 骨格: クローズ
    }
}

ユーザーは ConnectionCallback に業務処理だけ書けばよく、 接続取得 → 業務 → 例外変換 → 解放 という骨格は Spring 側に閉じ込められています。

同様に RestTemplate, TransactionTemplate, KafkaTemplate などの *Template クラスは すべて Template Method パターンです。 「〜Template」 という命名はこのパターン由来です。

コールバック (Lambda) で近代化する

伝統的な Template Method は 「継承ありき」 ですが、 現代の Java では コールバックに差分を渡す ことで、継承せずに同じ構造を作れます。

public final class ReportFlow {
    public static void run(
        OutputStream out,
        IoAction openHeader,
        IoAction writeBody,
        IoAction closeFooter
    ) throws IOException {
        openHeader.accept(out);
        writeBody.accept(out);
        closeFooter.accept(out);
    }

    @FunctionalInterface
    interface IoAction {
        void accept(OutputStream out) throws IOException;
    }
}

// 呼び出し側
ReportFlow.run(out,
    o -> o.write("id,name\n".getBytes()),
    o -> rows.forEach(r -> write(o, r)),
    o -> {}
);

判断軸:

  • 骨格が複雑 (フック多数、親に状態持たせたい) → 伝統的な継承
  • 骨格が単純 (3 ステップ程度) → コールバックで十分

落とし穴

1. 骨格を final にせずオーバーライドされる

子クラスで骨格メソッドまでオーバーライドされると、順序が崩れ、 「継承したつもりで同じフローのはずなのにバグる」 事態が起きます。 骨格は必ず final にしてください。

2. フックメソッドを多用して継承地獄

「こういう場合もあるかも」 で beforeXxx(), afterXxx(), onError() を乱立させると、 子クラスの実装者が「どこで何が呼ばれるのか」を追えなくなります。 フックは最小限 に。本当に必要になってから追加するのが安全。

3. 継承で捨てられない共通処理が重い

継承にすると、「骨格を使いたいだけなのに親の全処理を背負わされる」 ことがあります。 テストで親の初期化がネックになったら、Strategy や Decorator への移行 を検討。


まとめ: Template Method は 「処理の順序を強制する」 ための強力なパターン。 Spring の *Template 系の設計思想がまさにこれで、 「ユーザーは業務ロジックだけ書けばいい」API を作るときの基本テクニックです。 継承の重さが気になるなら、同じ構造をコールバックで組むのも手。

関連記事