「Template Method」パターンとは?サンプルを踏まえてわかりやすく解説!【Java】

  • LINEで送る
「Template Method」パターンとは?サンプルを踏まえてわかりやすく解説!【Java】

重要度:★★★★☆

 本記事ではGoFのデザインパターンのプログラムの振る舞いに関するパターンの一つである「Template Method」パターンを解説します。このパターンを一言で説明するならば、「抽象的な処理を親クラスで定義し、サブクラスで処理を実装することでオブジェクトの振る舞いを共通化するパターン」と言えるでしょう。「Template Method」パターンをサンプルを踏まえて解説します。

Template Methodパターンとは

 Template Methodパターンは、一連の処理手順(テンプレート)を親クラスに定義し、処理の一部をサブクラスで柔軟に差し替え可能にするデザインパターンです。

 Template Methodパターンの目的は、処理の流れを標準化しつつ、細部は柔軟に変更できる仕組みを提供することです。業務システムなどでは、「手順の流れは毎回同じだが、一部の内容は取引先や商品によって変わる」といった処理が多く発生します。このような場面で処理全体の一貫性を保ちつつ、必要な部分だけを変更できるのがこのパターンの強みです。

 実務での例で解説します。ECサイトで商品を購入する場合に、一定の流れがあるかと思います。

  1. カートの中身を確認
  2. 決済処理
  3. 在庫処理
  4. 発送処理
  5. 購入者への通知

 この中で、「カートの中身を確認」、「購入者への通知」は共通の処理で使えそうです。ただしそれ以外の処理については、決済方法が変わったり、電子書籍で在庫数を持っていなかったり、発送方法が変わったり、そもそも発送しなくていい商品だったりと、それぞれの商品によっては処理手順は同じでも内容が大きく変わってきます。

 このような状況の時にTemplate Methodを使えば、共通処理の流れは保ちつつ、異なる部分だけ柔軟に差し替えることができます

Template Methodパターンの構成要素

 Template Methodパターンの構成要素は主に2つあり、とてもシンプルなパターンです。

要素役割
AbstractClass
(抽象クラス)
・テンプレート(処理の流れ)を定義
・具体的な処理の一部は抽象メソッドとしてサブクラスに委ねる
ConcreteClass
(具体クラス)
・抽象クラスで定義された抽象メソッドを実装する
・処理の具体内容を定義する

Template Methodパターンの書き方

 実際に上記で触れた「ECサイトで商品を購入する処理フロー」の例でTemplate Methodパターンを実装します。

❌ Template Methodを使わない場合

 まずTemplate Methodパターンを利用しない方法で「ECサイトで商品を購入する処理フロー」の仕組みを実装するとどのようになるでしょうか。おそらく下記のようにそれぞれの購入フローでクラスを作り、自前で順番にそれぞれの手順であるserviceを実行するような構築になるかと思います。

// デジタルコンテンツの商品を購入するフロー
public class DigitalProductService {
    private PaymentService paymentService = new PaymentService();
    private InventoryService inventoryService = new InventoryService();
    private NotificationService notificationService = new NotificationService();

    public void processPurchase(String userId, String productCode, String email) {
        System.out.println("カート確認:" + productCode);
        paymentService.pay("クレジットカード");
        inventoryService.unlockDownload(userId, productCode);
        notificationService.sendPurchaseEmail(email);
        // 発送処理は不要なので記述しない
    }
}

// 物理的な商品を購入するフロー
public class PhysicalProductService {
    private PaymentService paymentService = new PaymentService();
    private InventoryService inventoryService = new InventoryService();
    private ShippingService shippingService = new ShippingService();
    private NotificationService notificationService = new NotificationService();

    public void processPurchase(String productCode, String email, String address) {
        System.out.println("カート確認:" + productCode);
        paymentService.pay("代金引換");
        inventoryService.reduceStock(productCode, 1);
        shippingService.scheduleShipment(address);
        notificationService.sendPurchaseEmail(email);
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("=== デジタル商品の購入 ===");
        DigitalProductService digital = new DigitalProductService();
        digital.processPurchase("user001", "DL123", "user001@example.com");

        System.out.println();

        System.out.println("=== 物理商品の購入 ===");
        PhysicalProductService physical = new PhysicalProductService();
        physical.processPurchase("PH456", "user002@example.com", "東京都新宿区1-2-3");
    }
}

 一見このコードは問題ないように見えます。他にも食品のフローを追加したいのであれば食品専用のクラスを作ればいいだけです。ではこのコードはどこが問題なのでしょうか。下記の表にまとめてみます。

問題点説明
共通処理の重複カート確認・決済・通知など、同じ処理が複数箇所に存在
拡張性が低い新しい商品タイプや決済方式を追加するたびに既存クラスを複製・修正する必要がある
保守性が悪い共通処理の「通知メール」の部分で「通知メールの形式を変更したい」場合、すべてのクラスで個別に修正が必要
処理の順序がバラバラになるリスク各クラスで手順を忘れたり、順番を変えたりする可能性がある

 このように、Template Methodを使わないとコードが散らばり、統一性も拡張性も失われます

 Template Methodを使う場合

 上記の問題点を解決できるのがTemplate Methodパターンです。では実際にTemplate Methodパターンを利用して「ECサイトで商品を購入する処理」の仕組みを実装していきます。

 まずは先ほどでも出ていましたが、それぞれの処理を担うserviceを作っていきます。

// 決済処理を行うserviceクラス
public class PaymentService {
    public void pay(PaymentMethod method) {
        System.out.println("決済処理中(方法:" + method.getLabel() + ")");
        // 実際にはAPIやDBとの連携がある想定
    }
}

// 在庫処理を行うserviceクラス
public class InventoryService {
    // DBの在庫数を減らす
    public void reduceStock(String productCode, int quantity) {
        System.out.println("在庫を " + quantity + " 個減らしました(商品:" + productCode + ")");
        // 実際にはAPIやDBとの連携がある想定
    }

    // ダウンロード回数を制限するメソッド
    public void unlockDownload(String userId, String productCode) {
        System.out.println("ユーザー " + userId + " にダウンロード権限を付与しました(商品:" + productCode + ")");
        // 実際にはAPIやDBとの連携がある想定
    }
}

// 配送処理を行うserviceクラス
public class ShippingService {
    public void scheduleShipment(String address) {
        System.out.println("「" + address + "」宛に発送手配しました");
        // 実際にはAPIやDBとの連携がある想定
    }
}

// 購入通知を行うserviceクラス
public class NotificationService {
    public void sendPurchaseEmail(String email) {
        System.out.println(email + " に購入完了メールを送信しました");
        // 実際にはAPIやDBとの連携がある想定
    }
}

 それぞれのクラスについては下記になります。

クラス役割
PaymentService(決済処理)・決済方法を指定して決済するメソッドを持つ
InventoryService(在庫処理)・DBに登録されている商品情報の在庫数を指定した数減らす
・ダウンロードを制限する
ShippingService(配送処理)・指定した住所へ配送を行う
NotificationService(NotificationService)・指定されたメールアドレスに購入通知を行う

 では実際にこれらのserviceを使って、商品の購入フローのテンプレートを作っていきます。このクラスは構成要素でいうところのAbstractClass(抽象クラス)に相当します。

public abstract class PurchaseFlow {
    protected PaymentService paymentService = new PaymentService();
    protected InventoryService inventoryService = new InventoryService();
    protected ShippingService shippingService = new ShippingService();
    protected NotificationService notificationService = new NotificationService();

    public final void processPurchase(String productCode, String userId, String email, String address) {
        // カート確認
        checkCart(productCode);

        // 決済処理
        processPayment();

        // 在庫処理
        updateStock(userId, productCode);

        // ダウンロード権限付与
        if (needsShipping()) {
            arrangeShipping(address);
        }
        
        // 購入完了通知
        notifyCustomer(email);
    }

    private void checkCart(String productCode) {
        System.out.println("カート確認:" + productCode);
    }

    protected abstract void processPayment();

    protected abstract void updateStock(String userId, String productCode);

    protected abstract boolean needsShipping();

    protected abstract void arrangeShipping(String address);

    private void notifyCustomer(String email) {
        notificationService.sendPurchaseEmail(email);
    }
}

 processPurchase()メソッドで処理フローを設定しています。注目して欲しいいただきたいのが、abstractになっているメソッドです。これらのメソッドは、継承先で必ずOverrideする必要があります。Overrideすることで、処理フローの一部を柔軟に書き換えることが可能です。

 またcheckCart()やnotifyCustomer()のメソッドはabstractになっていません。どのような商品を購入したとしても、カート内の確認や購入通知は統一されます。そのため継承先でもこれらのメソッドは書き換わることがなく、共通処理として扱われます。

 次にこのPurchaseFlowクラスを継承して、個別の処理フローを作っていきます。物理的な商品を購入するフローと、電子書籍を購入するフローで2つ処理フローを担うクラスを実装します。

// デジタル商品を購入するフローの個別クラス
public class DigitalProductPurchase extends PurchaseFlow {

    @Override
    protected void processPayment() {
        paymentService.pay(PaymentMethod.CREDIT_CARD);
    }

    @Override
    protected void updateStock(String userId, String productCode) {
        inventoryService.unlockDownload(userId, productCode);
    }

    @Override
    protected boolean needsShipping() {
        return false;
    }

    @Override
    protected void arrangeShipping(String address) {
        // デジタル商品は配送不要
        System.out.println("デジタル商品は配送不要です");
    }
}
// 物理商品を購入するフローの個別クラス
public class PhysicalProductPurchase extends PurchaseFlow {

    @Override
    protected void processPayment() {
        paymentService.pay(PaymentMethod.CASH);
    }

    @Override
    protected void updateStock(String userId, String productCode) {
        inventoryService.reduceStock(productCode, 1);
    }

    @Override
    protected boolean needsShipping() {
        return true;
    }

    @Override
    protected void arrangeShipping(String address) {
        shippingService.scheduleShipment(address);
    }
}

 abstractになっているメソッドをOverrideして、個別フローごとの処理を記述しています。共通部分となるメソッドはabstractなメソッドではないため、コンパイルエラーとなることもありません。

 では最後に、これらの個別フローを利用して、商品を購入してみましょう。

public class TemplateMethodSample {
    public static void main(String[] args) {

        // ディジタル商品購入フロー
        PurchaseFlow digital = new DigitalProductPurchase();
        System.out.println("=== デジタル商品購入 ===");
        digital.processPurchase("DL123", "user001", "user001@example.com", "");

        System.out.println();

        // 物理商品購入フロー
        PurchaseFlow physical = new PhysicalProductPurchase();
        System.out.println("=== 物理商品購入 ===");
        physical.processPurchase("PH456", "user002", "user002@example.com", "東京都新宿区1-2-3");
    }
}
>> 実行結果
=== デジタル商品購入 ===
カート確認:DL123
決済処理中(方法:クレジットカード)
ユーザー user001 にダウンロード権限を付与しました(商品:DL123)
user001@example.com に購入完了メールを送信しました

=== 物理商品購入 ===
カート確認:PH456
決済処理中(方法:現金)
在庫を 1 個減らしました(商品:PH456)
「東京都新宿区1-2-3」宛に発送手配しました
user002@example.com に購入完了メールを送信しました

 これでそれぞれの処理フローを、テンプレートを利用して実装することができました。共通処理をまとめることができているため、保守性と拡張性が高い実装となりました。

メリットとデメリット

メリット

メリット説明
処理の流れを固定化できるアルゴリズムの全体構造を抽象クラスで管理でき、ブレない処理を実現できる
処理の一部だけ柔軟に変更可能必要な部分だけサブクラスでオーバーライドできる
コードの再利用性向上共通処理は抽象クラスにまとめることでDRY原則※1を守れる
単語の意味※1 DRY原則とは「Don’t Repeat Yourself※自分自身を繰り返すな!」という意味でコード中に同じコードや処理を二重三重に記述せずに1つの場所にまとめること

デメリット

デメリット説明
継承に依存するJavaでは多重継承ができないため、柔軟性が制限される場合がある
サブクラス間の依存が発生しやすい抽象クラスに手を加えると、すべてのサブクラスに影響が出る

 Template Methodパターンには上記のメリットとデメリットがあります。これらのメリットとデメリットから、Template Methodパターンが有効なシチュエーションには下記のような場合があります。

  • 処理の流れを統一したい場合
    • 全体のアルゴリズムや手順は変更してはならないが、一部の処理だけはケースに応じて差し替える必要があるとき
    • 例)データインポート処理で、CSV / Excel / JSONなど形式ごとの読み込み処理だけを変更し、その他の検証・登録処理は共通としたい場合
  • 共通フローの再利用と、重複排除(DRY原則)を両立したい場合
    • 複数のサブクラスで同じような処理の順序や共通処理があるが、それらを一か所にまとめたいとき
    • 例)ECサイトにおける購入処理フローで、デジタル商品と物理商品の購入処理に共通点(カート確認・通知など)が多く、差分(決済方法・発送)が一部に限られているケース

まとめ

 Template Methodパターンは、「処理の流れを統一しながら、柔軟に拡張したい業務処理」に非常に有効です。今回のような商品購入フローは、決済方法や商品タイプによって一部の処理だけを変えたい場合には特に威力を発揮するパターンと言えるでしょう。

 本記事で利用したサンプルケースのクラス図は下記になります。

 最後に「Template Method」パターンとは「抽象的な処理を親クラスで定義し、サブクラスで処理を実装することでオブジェクトの振る舞いを共通化するパターン」です。うまく使っていきましょう。

 本記事で利用したコードは下記のgit上で公開しております。

https://github.com/tamotech1028/template-method


デザインパターン講座 目次

第1章 導入

第2章 オブジェクトの「生成」に関するパターン

第3章 プログラムの「構造」に関するパターン

第4章 オブジェクトの「振る舞い」に関するパターン

最新の投稿

SNSでもご購読できます。

コメント

コメントを残す