【ITニュース解説】Strategy Design Pattern
2025年10月05日に「Dev.to」が公開したITニュース「Strategy Design Pattern」について初心者にもわかりやすく解説しています。
ITニュース概要
ストラテジーパターンは、ECサイトの支払い方法のように、実行時に処理ロジックを柔軟に切り替えたい場合に使う設計手法だ。同じような処理が複数の場所で重複するのを防ぎ、新しい支払い方法を追加する際も、既存コードへの影響を最小限に抑えられる。コードの保守性や拡張性が大幅に向上する。
ITニュース解説
ITシステム開発において、状況に応じて処理を柔軟に切り替えたい場面は数多く存在する。例えば、Amazonのような大規模なオンラインショッピングサイトを想像してみる。商品を購入する際には、クレジットカード、デビットカード、PayPal、あるいはQRコード決済など、様々な支払い方法の中から利用者は自由に選択できる。さらに、Prime会員の登録など、他のサービスを利用する際にも、同じ支払い方法の選択肢が期待されるだろう。
このような「状況によって処理内容を変えたいが、その処理の選択肢は複数の場所で共通して使われる」という要件を、どのように効率的に実現し、将来的な変更にも強くしていけばよいのか。最初の段階で安易な実装をしてしまうと、後になって大きな問題を引き起こす可能性がある。
具体例として、もしあなたがショッピングカートの支払い処理を実装するシステムエンジニアだと仮定してみる。最初の発想として、以下のようなコードを考えるかもしれない。
ShoppingCartというクラスには、checkoutという支払い処理を行うメソッドがある。このメソッドは、paymentTypeという引数で「CREDIT(クレジットカード)」「PAYPAL(PayPal)」「UPI(QRコード決済)」などの支払い方法の種類を受け取り、その種類に応じて適切な支払い処理を実行する。
class ShoppingCart {
public void checkout(String paymentType, int amount) {
if (paymentType.equals("CREDIT")) {
System.out.println("Paid " + amount + " with Credit Card");
} else if (paymentType.equals("PAYPAL")) {
System.out.println("Paid " + amount + " with PayPal");
} else if (paymentType.equals("UPI")) {
System.out.println("Paid " + amount + " with UPI");
}
}
}
同様に、オンラインサービスの「Subscription(定期購読)」の支払い処理も実装する必要があるとしよう。ここでも同じように、クレジットカード、PayPal、QRコード決済などの選択肢を提供したい。そうすると、Subscriptionクラスのcheckoutメソッドも、ShoppingCartクラスとほとんど同じコードになるだろう。
class Subscription {
public void checkout(String paymentType, int amount) {
if (paymentType.equals("CREDIT")) {
System.out.println("Paid " + amount + " with Credit Card");
} else if (paymentType.equals("PAYPAL")) {
System.out.println("Paid " + amount + " with PayPal");
} else if (paymentType.equals("UPI")) {
System.out.println("Paid " + amount + " with UPI");
}
}
}
この二つのクラスのコードは、見た目も内容もほとんど同じであることに気づくだろう。このような状態を「コードの冗長性(Code Redundancy)」と呼ぶ。この冗長なコードは、現時点では問題ないように見えるかもしれないが、将来的に大きな課題となる。
例えば、新しい支払い方法として「Cash(現金払い)」を追加することになった場合を想像してみてほしい。あなたはShoppingCartクラスとSubscriptionクラスの両方を開き、それぞれのcheckoutメソッドにelse if (paymentType.equals("CASH"))という条件と処理を追加する必要がある。もし支払い方法が5種類、その支払い処理が関わるクラスが10種類あったらどうなるだろうか。すべてのクラスに同じ変更を加える手間がかかり、一つでも変更を忘れるとシステム全体で不整合が生じる可能性がある。これは開発効率を著しく低下させ、バグの温床となる。
このような問題を解決し、コードの柔軟性、拡張性、保守性を高めるための設計手法の一つに、「Strategy Design Pattern(ストラテジーデザインパターン)」がある。このパターンは、「アルゴリズム(処理の手順)の族(種類)を定義し、それぞれをカプセル化(独立した部品としてまとめる)し、それらを交換可能にする」という考え方に基づいている。簡単に言えば、具体的な処理内容を別々の部品として用意しておき、本体のプログラムはどの部品を使うかを切り替えるだけで済むようにする、という手法である。
ストラテジーパターンを適用した場合、先ほどの支払い処理の例はどのように変わるだろうか。
まず、「支払い」という共通の「振る舞い」を定義するPaymentStrategyという「インターフェース」を用意する。インターフェースとは、「〇〇の機能を持つクラスは、必ずこのメソッドを実装しなければならない」という「契約」のようなものである。ここでは、pay(int amount)というメソッドを定義する。これにより、「支払う」という行為は共通だが、具体的な支払い方法は個別のクラスに任せる、という構造の土台ができる。
interface PaymentStrategy {
void pay(int amount);
}
次に、具体的な支払い方法ごとのクラスを作成する。これらは全てPaymentStrategyインターフェースを実装する。
CreditCardPaymentクラス:クレジットカードでの支払い処理を具体的に実装する。PayPalPaymentクラス:PayPalでの支払い処理を具体的に実装する。UpiPaymentクラス:QRコード決済での支払い処理を具体的に実装する。
各クラスは、それぞれのpayメソッドの中で、独自の支払いロジックを記述する。例えば、CreditCardPaymentクラスのpayメソッドは「クレジットカードで〇〇を支払った」と出力する。
class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " with Credit Card");
}
}
// 他のPayPalPayment, UpiPaymentも同様
そして、ShoppingCartクラスとSubscriptionクラスは、もはや具体的な支払い方法の知識を持つ必要がなくなる。これらのクラスは、PaymentStrategyインターフェース型のオブジェクトを内部に持つだけになる。このインターフェース型のオブジェクトは、実行時にどの具体的な支払い方法(CreditCardPaymentなのかPayPalPaymentなのか)をセットするかで、振る舞いが変わる。
ShoppingCartクラスには、setPaymentStrategyというメソッドが追加される。このメソッドを使って、どのPaymentStrategy(例えばnew CreditCardPayment())をShoppingCartに設定するかを決める。そして、checkoutメソッドでは、以前のようなif-else ifの分岐は一切なくなり、単に内部に持っているpaymentStrategyオブジェクトのpayメソッドを呼び出すだけになる。
class ShoppingCart {
private PaymentStrategy paymentStrategy; // 支払い戦略を保持する
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy; // どの戦略を使うか設定できる
}
public void checkout(int amount) {
paymentStrategy.pay(amount); // 設定された戦略で支払う
}
}
// Subscriptionクラスも同様
これにより、ShoppingCartやSubscriptionといったクラスは、具体的な支払い方法(クレジットカードなのかPayPalなのか)を知る必要がなくなり、「支払い」という共通の行為だけを依頼できるようになる。これがストラテジーパターンの核心である。
このストラテジーパターンを適用することで、以下のような大きな利点が得られる。
第一に、コードの利便性が高まる。利用する支払い方法を柔軟に設定・変更できるようになり、コードの利用者にとっても扱いやすくなる。
第二に、高い拡張性が実現される。例えば、新たに「コンビニ払い」という支払い方法を追加したい場合、PaymentStrategyインターフェースを実装したConvenienceStorePaymentという新しいクラスを一つ作成するだけで済む。既存のShoppingCartやSubscriptionクラスのコードは一切変更する必要がない。これは、新しい機能を追加する際に既存の安定したコードを触らずに済むため、バグのリスクを最小限に抑えることができる。
第三に、保守性が向上する。各支払い方法のロジックがそれぞれのクラスに独立してまとめられているため、特定の支払い方法に修正や変更が必要になった場合でも、その支払い方法を担当するクラスだけを修正すればよい。他のクラスへの影響を気にすることなく、効率的にメンテナンスを進められる。
第四に、コードの冗長性が排除される。if-else ifの分岐が複数箇所に重複して記述されることがなくなるため、コード全体の量も減り、見通しが良くなる。
最後に、システム全体の信頼性が高まる。コードの重複が減ることで、同じロジックを複数箇所で修正し忘れるといったヒューマンエラーが減少する。また、各機能が独立した部品としてテストしやすくなるため、品質向上にも貢献する。
このように、ストラテジーデザインパターンは、状況に応じてアルゴリズムや処理内容を動的に切り替えたい場合に非常に有効な設計手法である。特に、新しい機能の追加や変更が頻繁に発生するシステムにおいて、その真価を発揮し、柔軟で保守性の高いソフトウェア開発を強力に後押しする。システムエンジニアとして、このような設計パターンを理解し、適切に活用することは、将来性の高いシステムを構築するために不可欠なスキルとなるだろう。