デザインパターン:戦略パターン入門

はじめに

先日、『Alibaba Java開発規約』を読んでいたところ、以下のような推奨規格が見つかりました:

3層を超えるif-elseの論理判断コードでは、ガード節、戦略パターン、状態パターンなどを利用して実装することができます

この「戦略パターン(Strategy Pattern)」がどのような解決策なのか気になり、調べてみることにしました。

定義:

戦略パターンは、一連のアルゴリズムを定義し、それぞれのアルゴリズムをカプセル化し、相互に交換可能にします。戦略パターンは、アルゴリズムを使用するクライアントから独立してアルゴリズムが変化できるようにします。

構造

戦略(Strategy)

サポートされるすべてのアルゴリズムの共通インターフェースを定義します。Contextはこのインターフェースを使用して、ConcreteStrategyで定義されたアルゴリズムを呼び出します。

戦略の実装(ConcreteStrategy)

Strategyインターフェースを使用して特定のアルゴリズムを実装します。

コンテキスト(Context)

ConcreteStrategyオブジェクトを使用して設定します。

Strategyオブジェクトへの参照を維持します。

StrategyがContextのデータにアクセスできるようにインターフェースを定義できます。

事例

あるECサイトのショッピングカートを設計する必要があり、以下のような設計を考えました。

まず、支払いインターフェースを作成します:

/**
 * 支払いインターフェース
 */
public interface PaymentMethod {
    // すべての支払い方法で決済が可能
    public void pay(double amount); 
}

クレジットカード支払いクラスを実装します:

/**
 * クレジットカード支払い
 */
public class CreditCardPayment implements PaymentMethod {
    private String cardNumber;
    private String name;
    
    public CreditCardPayment(String cardNumber, String name) {
        this.cardNumber = cardNumber;
        this.name = name;
    }
    
    public void pay(double amount) {
        System.out.println("カード番号 " + this.cardNumber + " の名義 " + this.name + " で " + amount + " 円をクレジットカード決済しました!");
    }
}

次に、銀行振込支払いクラスを実装します:

/**
 * 銀行振込支払い
 */
public class BankTransferPayment implements PaymentMethod {
    private String bankName;
    private String accountNumber;
    
    public BankTransferPayment(String bankName, String accountNumber) {
        this.bankName = bankName;
        this.accountNumber = accountNumber;
    }
    
    public void pay(double amount) {
        System.out.println(this.bankName + " の口座番号 " + this.accountNumber + " から " + amount + " 円を銀行振込しました!");
    }
}

さらに、電子マネー支払いクラスを実装します:

/**
 * 電子マネー支払い
 */
public class ElectronicMoneyPayment implements PaymentMethod{
    private String walletName;
    
    public ElectronicMoneyPayment(String walletName) {
        this.walletName = walletName;
    }
    
    public void pay(double amount) {
        System.out.println(this.walletName + " から " + amount + " 円を電子マネー決済しました!");
    }
}

これで基本的な支払い機能は実装できました。しかし、後ほどクライアント側で割引機能を追加する必要が生じました。この割引機能を実装するため、各支払いクラスに`applyDiscount()`メソッドを追加することにしました。

クレジットカード支払いクラス

/**
 * クレジットカード支払い
 */
public class CreditCardPayment implements PaymentMethod {
    private String cardNumber;
    private String name;
    
    public CreditCardPayment(String cardNumber, String name) {
        this.cardNumber = cardNumber;
        this.name = name;
    }
    
    public void pay(double amount) {
        System.out.println("カード番号 " + this.cardNumber + " の名義 " + this.name + " で " + amount + " 円をクレジットカード決済しました!");
    }
    
    public void applyDiscount(double discountRate) {
        System.out.println("クレジットカード割引 " + (discountRate * 100) + "% を適用しました!");
    }
}

銀行振込支払いクラス

/**
 * 銀行振込支払い
 */
public class BankTransferPayment implements PaymentMethod {
    private String bankName;
    private String accountNumber;
    
    public BankTransferPayment(String bankName, String accountNumber) {
        this.bankName = bankName;
        this.accountNumber = accountNumber;
    }
    
    public void pay(double amount) {
        System.out.println(this.bankName + " の口座番号 " + this.accountNumber + " から " + amount + " 円を銀行振込しました!");
    }
    
    public void applyDiscount(double discountRate) {
        System.out.println("銀行振込割引 " + (discountRate * 100) + "% を適用しました!");
    }
}

しかし、支払い方法はこれらだけではありません。多くの支払い方法が追加される可能性があり、それぞれに割引機能を実装する必要があります。これでは開発工数が大幅に増加してしまいます。

解決策

ここで戦略パターンを使用して、振る舞いを切り出します。`PaymentMethod`を:

/**
 * 支払いインターフェース
 */
public interface PaymentMethod {
    void pay(double amount);
}

/**
 * クレジットカード支払い
 */
public class CreditCardPayment implements PaymentMethod {
    private String cardNumber;
    private String name;
    
    public CreditCardPayment(String cardNumber, String name) {
        this.cardNumber = cardNumber;
        this.name = name;
    }
    
    public void pay(double amount) {
        System.out.println("カード番号 " + this.cardNumber + " の名義 " + this.name + " で " + amount + " 円をクレジットカード決済しました!");
    }
}

/**
 * 銀行振込支払い
 */
public class BankTransferPayment implements PaymentMethod {
    private String bankName;
    private String accountNumber;
    
    public BankTransferPayment(String bankName, String accountNumber) {
        this.bankName = bankName;
        this.accountNumber = accountNumber;
    }
    
    public void pay(double amount) {
        System.out.println(this.bankName + " の口座番号 " + this.accountNumber + " から " + amount + " 円を銀行振込しました!");
    }
}

/**
 * 電子マネー支払い
 */
public class ElectronicMoneyPayment implements PaymentMethod {
    private String walletName;
    
    public ElectronicMoneyPayment(String walletName) {
        this.walletName = walletName;
    }
    
    public void pay(double amount) {
        System.out.println(this.walletName + " から " + amount + " 円を電子マネー決済しました!");
    }
}

割引機能用のインターフェースと実装クラス:

/**
 * 割引インターフェース
 */
public interface DiscountStrategy {
    void applyDiscount(double amount);
}

/**
 * クレジットカード割引
 */
public class CreditCardDiscount implements DiscountStrategy {
    public void applyDiscount(double amount) {
        System.out.println("クレジットカード割引 5% を適用しました!");
    }
}

/**
 * 銀行振込割引
 */
public class BankTransferDiscount implements DiscountStrategy {
    public void applyDiscount(double amount) {
        System.out.println("銀行振込割引 3% を適用しました!");
    }
}

/**
 * 電子マネー割引
 */
public class ElectronicMoneyDiscount implements DiscountStrategy {
    public void applyDiscount(double amount) {
        System.out.println("電子マネー割引 10% を適用しました!");
    }
}

コンテキスト(Context)

/**
 * ショッピングカートコンテキスト
 */
public class ShoppingCart {
    private PaymentMethod paymentMethod = null;
    private DiscountStrategy discountStrategy = null;
    
    public ShoppingCart(PaymentMethod paymentMethod) {
        this.paymentMethod = paymentMethod;
    }
    
    public ShoppingCart(PaymentMethod paymentMethod, DiscountStrategy discountStrategy) {
        this.paymentMethod = paymentMethod;
        this.discountStrategy = discountStrategy;
    }
    
    public void checkout(double amount) {
        if (discountStrategy != null) {
            discountStrategy.applyDiscount(amount);
        }
        
        if (paymentMethod != null) {
            paymentMethod.pay(amount);
        }
    }
}

使用例

public class ShoppingCartDemo {
    public static void main(String[] args) {
        ShoppingCart creditCardCart = new ShoppingCart(
            new CreditCardPayment("1234-5678-9012-3456", "Taro Yamada"),
            new CreditCardDiscount()
        );
        creditCardCart.checkout(10000);
        
        ShoppingCart bankTransferCart = new ShoppingCart(
            new BankTransferPayment("ABC銀行", "1234567"),
            new BankTransferDiscount()
        );
        bankTransferCart.checkout(15000);
        
        ShoppingCart electronicMoneyCart = new ShoppingCart(
            new ElectronicMoneyPayment("Suica")
        );
        electronicMoneyCart.checkout(5000);
    }
}

まとめ

戦略パターンはアルゴリズムの自由な切り替えと拡張に使用され、広く応用されているデザインパターンの1つです。戦略パターンは特定の問題を解決するアルゴリズム族に対応しており、ユーザーはそのアルゴリズム族から任意のアルゴリズムを選択して問題を解決できます。同時に、アルゴリズムの交換や新しいアルゴリズムの追加を容易に行うことができます。アルゴリズムのカプセル化、再利用、切り替えに関わる場合は、戦略パターンの使用を検討できます。

1. 主な利点

戦略パターンの主な利点は以下の通りです:

  • 戦略パターンは「オープン・クローズド原則」への完璧なサポートを提供します。ユーザーは既存システムを修正することなくアルゴリズムまたは振る舞いを選択でき、新しいアルゴリズムまたは振る舞いを柔軟に追加できます。
  • 戦略パターンは関連するアルゴリズム族を管理する方法を提供します。戦略クラスの階層構造はアルゴリズムまたは振る舞いの族を定義し、継承を適切に使用することで共通のコードを抽象戦略クラスに移動できます。これにより、重複コードを回避できます。
  • 戦略パターンは継承関係の代替手段を提供します。戦略パターンを使用しない場合、アルゴリズムを使用する環境クラスにはいくつかのサブクラスが存在し、それぞれのサブクラスが異なるアルゴリズムを提供する可能性があります。しかし、これによりアルゴリズムの使用とアルゴリズム自体が混在し、「単一責任原則」に違反します。どのアルゴリズムを使用するかのロジックとアルゴリズム自体の実装ロジックが混在し、独立して進化することができません。また、継承を使用してもプログラム実行時のアルゴリズムまたは振る舞いの動的切り替えを実現できません。
  • 戦略パターンを使用すると多重条件選択文を回避できます。多重条件選択文は保守が困難です。どのアルゴリズムまたは振る舞いを使用するかのロジックとアルゴリズムまたは振る舞い自体の実装ロジックを混在させ、それらをすべて巨大な多重条件選択文にハードコーディングします。これは環境クラスを直接継承する方法よりも原始的で時代遅れです。
  • 戦略パターンはアルゴリズムの再利用メカニズムを提供します。アルゴリズムを個別に抽出して戦略クラスにカプセル化するため、異なる環境クラスがこれらの戦略クラスを容易に再利用できます。

2. 主な欠点

戦略パターンの主な欠点は以下の通りです:

  • クライアントはすべての戦略クラスを知っており、どの戦略クラスを使用するかを自ら決定する必要があります。これはつまり、クライアントはこれらのアルゴリズムの違いを理解し、適切なアルゴリズムを適時選択する必要があることを意味します。言い換えれば、戦略パターンはクライアントがすべてのアルゴリズムまたは振る舞いを知っている場合にのみ適用されます。
  • 戦略パターンはシステムに多くの具体的な戦略クラスを生成させます。どのような小さな変更でも、システムに新しい具体的な戦略クラスを追加することになります。
  • クライアント側で複数の戦略クラスを同時に使用できません。つまり、戦略パターンを使用する場合、クライアントは一度に1つの戦略クラスしか使用できません。ある戦略クラスで一部の機能を完了してから、別の戦略クラスを使用して残りの機能を完了するようなケースはサポートされていません。

3. 適用シーン

以下の場合には戦略パターンの使用を検討できます:

  • システムが動的にいくつかのアルゴリズムの中から1つを選択する必要がある場合、これらのアルゴリズムを個々の具体的なアルゴリズムクラスにカプセル化し、これらの具体的なアルゴリズムクラスはすべて抽象アルゴリズムクラスのサブクラスとします。言い換えれば、これらの具体的なアルゴリズムクラスはすべて同じインターフェースを持ち、「リスコフの置換原則」とオブジェクト指向の多態性に基づき、クライアントは任意の具体的なアルゴリズムクラスを選択し、抽象アルゴリズムクラスのオブジェクトを1つ保持するだけで済みます。
  • オブジェクトに多くの振る舞いがある場合、適切なパターンを使用しないと、これらの振る舞いは多重条件選択文で実装するしかありません。この場合、戦略パターンを使用してこれらの振る舞いを対応する具体的な戦略クラスに移動し、保守が困難な多重条件選択文の使用を回避できます。
  • クライアントがアルゴリズムに関連する複雑なデータ構造を知らないようにしたい場合、具体的な戦略クラスでアルゴリズムと関連するデータ構造をカプセル化することで、アルゴリズムの機密性と安全性を高めることができます。

タグ: デザインパターン 戦略パターン Java オブジェクト指向プログラミング アルゴリズム

7月19日 20:46 投稿