0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

全30回:静的と動的でどう違うのか、JavaとPythonで学ぶデザインパターン - Day 23【デザインパターン】Strategyパターン:アルゴリズムを動的に切り替える

Posted at

はじめに

皆さん、こんにちは!「JavaとPythonで比べるデザインパターン」シリーズの第23回目です。

今回は、アルゴリズムのファミリーをカプセル化し、それらを相互に交換可能にするためのStrategy(ストラテジー)パターンについて解説します。


💡 Strategyパターンとは?

Strategyパターンは、特定のタスクを実行するためのアルゴリズムをクラスとしてカプセル化し、実行時にクライアントがそのアルゴリズムを動的に切り替えられるようにする振る舞いパターンです。

身近な例:決済システム

例えるなら、決済方法です。

  • コンテキスト: お店での支払いシステム
  • ストラテジー: 現金、クレジットカード、モバイル決済、ポイント支払いなど
  • クライアント: お客さん

お客さん(クライアント)は状況に応じて最適な支払い方法を選び、店員(コンテキスト)に伝えます。店員は、選ばれた方法に従って決済処理を行います。支払い方法が増えても、店員の基本的な処理は変わりません。

パターンの構成要素

  1. Strategy(ストラテジー): 共通のインターフェースを定義
  2. ConcreteStrategy(具象ストラテジー): 具体的なアルゴリズムを実装
  3. Context(コンテキスト): ストラテジーを使用するクラス

主な目的

  • アルゴリズムの分離: 特定のアルゴリズムをメインロジックから分離
  • 柔軟な切り替え: 実行時にアルゴリズムを動的に変更
  • オープン・クローズドの原則: 拡張に開いて、変更に閉じた設計

☕ Javaでの実装:厳格なインターフェースとコンポジション

Javaは、アルゴリズムを表現するインターフェースを厳格に定義することで、Strategyパターンを安全に実装できます。

実装例:価格計算システム

商品の価格を計算する際の異なる割引戦略を例に実装します。

// JavaでのStrategyパターンの実装例

// Strategy: 割引戦略のインターフェース
interface DiscountStrategy {
    double applyDiscount(double originalPrice);
    String getDescription();
}

// ConcreteStrategy: 10%割引
class PercentageDiscount implements DiscountStrategy {
    private final double percentage;
    
    public PercentageDiscount(double percentage) {
        this.percentage = percentage;
    }
    
    @Override
    public double applyDiscount(double originalPrice) {
        return originalPrice * (1.0 - percentage / 100.0);
    }
    
    @Override
    public String getDescription() {
        return percentage + "% 割引";
    }
}

// ConcreteStrategy: 固定額割引
class FixedAmountDiscount implements DiscountStrategy {
    private final double amount;
    
    public FixedAmountDiscount(double amount) {
        this.amount = amount;
    }
    
    @Override
    public double applyDiscount(double originalPrice) {
        double discountedPrice = originalPrice - amount;
        return Math.max(discountedPrice, 0); // 負の値を防ぐ
    }
    
    @Override
    public String getDescription() {
        return amount + "円 割引";
    }
}

// ConcreteStrategy: 段階的割引
class TieredDiscount implements DiscountStrategy {
    @Override
    public double applyDiscount(double originalPrice) {
        if (originalPrice >= 10000) {
            return originalPrice * 0.8; // 20%割引
        } else if (originalPrice >= 5000) {
            return originalPrice * 0.9; // 10%割引
        } else {
            return originalPrice; // 割引なし
        }
    }
    
    @Override
    public String getDescription() {
        return "段階的割引(5000円以上10%、10000円以上20%)";
    }
}

// Context: 価格計算クラス
class PriceCalculator {
    private DiscountStrategy discountStrategy;
    
    public PriceCalculator() {
        // デフォルトは割引なし
        this.discountStrategy = new DiscountStrategy() {
            @Override
            public double applyDiscount(double originalPrice) {
                return originalPrice;
            }
            
            @Override
            public String getDescription() {
                return "割引なし";
            }
        };
    }
    
    public void setDiscountStrategy(DiscountStrategy discountStrategy) {
        if (discountStrategy != null) {
            this.discountStrategy = discountStrategy;
        }
    }
    
    public double calculatePrice(double originalPrice) {
        if (originalPrice < 0) {
            throw new IllegalArgumentException("価格は0以上である必要があります");
        }
        return discountStrategy.applyDiscount(originalPrice);
    }
    
    public String getCurrentDiscountDescription() {
        return discountStrategy.getDescription();
    }
}

// 使用例
public class StrategyPatternDemo {
    public static void main(String[] args) {
        PriceCalculator calculator = new PriceCalculator();
        double originalPrice = 8000;
        
        System.out.println("元の価格: " + originalPrice + "円");
        
        // 10%割引を適用
        calculator.setDiscountStrategy(new PercentageDiscount(10));
        System.out.printf("%s: %.0f円\n", 
                         calculator.getCurrentDiscountDescription(),
                         calculator.calculatePrice(originalPrice));
        
        // 500円引きを適用
        calculator.setDiscountStrategy(new FixedAmountDiscount(500));
        System.out.printf("%s: %.0f円\n", 
                         calculator.getCurrentDiscountDescription(),
                         calculator.calculatePrice(originalPrice));
        
        // 段階的割引を適用
        calculator.setDiscountStrategy(new TieredDiscount());
        System.out.printf("%s: %.0f円\n", 
                         calculator.getCurrentDiscountDescription(),
                         calculator.calculatePrice(originalPrice));
    }
}

実行結果:

元の価格: 8000円
10.0% 割引: 7200円
500.0円 割引: 7500円
段階的割引(5000円以上10%、10000円以上20%): 7200円

Javaの特徴

  • 型安全性: インターフェースにより厳格な型チェック
  • 明示的な設計: 各ストラテジーが独立したクラス
  • 拡張性: 新しい割引方法を簡単に追加可能

🐍 Pythonでの実装:関数オブジェクトとシンプルさ

Pythonの関数は第一級オブジェクトであるため、Strategyパターンをよりシンプルに実装できます。

実装例1:関数ベースのアプローチ

# PythonでのStrategyパターンの実装例(関数ベース)

from typing import Callable, Protocol
from abc import ABC, abstractmethod

# Strategy Protocol(型ヒント用)
class DiscountStrategy(Protocol):
    def __call__(self, price: float) -> tuple[float, str]:
        """価格に割引を適用し、(割引後価格, 説明)を返す"""
        ...

# 具体的な割引戦略(関数として実装)
def percentage_discount(percentage: float) -> DiscountStrategy:
    """パーセント割引を作成する高階関数"""
    def apply_discount(price: float) -> tuple[float, str]:
        discounted_price = price * (1.0 - percentage / 100.0)
        description = f"{percentage}% 割引"
        return discounted_price, description
    return apply_discount

def fixed_amount_discount(amount: float) -> DiscountStrategy:
    """固定額割引を作成する高階関数"""
    def apply_discount(price: float) -> tuple[float, str]:
        discounted_price = max(price - amount, 0)  # 負の値を防ぐ
        description = f"{amount}円 割引"
        return discounted_price, description
    return apply_discount

def tiered_discount(price: float) -> tuple[float, str]:
    """段階的割引"""
    if price >= 10000:
        return price * 0.8, "段階的割引(20%)"
    elif price >= 5000:
        return price * 0.9, "段階的割引(10%)"
    else:
        return price, "割引なし"

def no_discount(price: float) -> tuple[float, str]:
    """割引なし"""
    return price, "割引なし"

# Context: 価格計算クラス
class PriceCalculator:
    def __init__(self, strategy: DiscountStrategy = no_discount):
        self._strategy = strategy
    
    def set_strategy(self, strategy: DiscountStrategy) -> None:
        """割引戦略を設定"""
        self._strategy = strategy
    
    def calculate_price(self, original_price: float) -> tuple[float, str]:
        """価格を計算し、(割引後価格, 説明)を返す"""
        if original_price < 0:
            raise ValueError("価格は0以上である必要があります")
        return self._strategy(original_price)

# 使用例
def main():
    calculator = PriceCalculator()
    original_price = 8000
    
    print(f"元の価格: {original_price}")
    
    # 10%割引を適用
    calculator.set_strategy(percentage_discount(10))
    price, desc = calculator.calculate_price(original_price)
    print(f"{desc}: {price:.0f}")
    
    # 500円引きを適用
    calculator.set_strategy(fixed_amount_discount(500))
    price, desc = calculator.calculate_price(original_price)
    print(f"{desc}: {price:.0f}")
    
    # 段階的割引を適用
    calculator.set_strategy(tiered_discount)
    price, desc = calculator.calculate_price(original_price)
    print(f"{desc}: {price:.0f}")

if __name__ == "__main__":
    main()

実装例2:クラスベースのアプローチ

# Pythonでのクラスベース実装

from abc import ABC, abstractmethod

class DiscountStrategy(ABC):
    """割引戦略の抽象基底クラス"""
    
    @abstractmethod
    def apply_discount(self, price: float) -> float:
        """割引を適用した価格を返す"""
        pass
    
    @abstractmethod
    def get_description(self) -> str:
        """割引の説明を返す"""
        pass

class PercentageDiscount(DiscountStrategy):
    """パーセント割引"""
    
    def __init__(self, percentage: float):
        self.percentage = percentage
    
    def apply_discount(self, price: float) -> float:
        return price * (1.0 - self.percentage / 100.0)
    
    def get_description(self) -> str:
        return f"{self.percentage}% 割引"

class FixedAmountDiscount(DiscountStrategy):
    """固定額割引"""
    
    def __init__(self, amount: float):
        self.amount = amount
    
    def apply_discount(self, price: float) -> float:
        return max(price - self.amount, 0)
    
    def get_description(self) -> str:
        return f"{self.amount}円 割引"

class PriceCalculator:
    def __init__(self, strategy: DiscountStrategy = None):
        self._strategy = strategy
    
    def set_strategy(self, strategy: DiscountStrategy) -> None:
        self._strategy = strategy
    
    def calculate_price(self, original_price: float) -> float:
        if self._strategy is None:
            return original_price
        return self._strategy.apply_discount(original_price)
    
    def get_current_discount_description(self) -> str:
        if self._strategy is None:
            return "割引なし"
        return self._strategy.get_description()

Pythonの特徴

  • 柔軟性: 関数、クラス、ラムダ式など様々なアプローチが可能
  • 簡潔性: 関数を直接ストラテジーとして使用可能
  • 動的性: 実行時の型チェックが緩やか

🔄 実践的な応用例

1. ソートアルゴリズムの切り替え

// Java
interface SortStrategy<T extends Comparable<T>> {
    void sort(List<T> list);
    String getName();
}

class QuickSort<T extends Comparable<T>> implements SortStrategy<T> {
    @Override
    public void sort(List<T> list) {
        Collections.sort(list); // 実際にはQuickSortの実装
    }
    
    @Override
    public String getName() {
        return "クイックソート";
    }
}

class MergeSort<T extends Comparable<T>> implements SortStrategy<T> {
    @Override
    public void sort(List<T> list) {
        // マージソートの実装
    }
    
    @Override
    public String getName() {
        return "マージソート";
    }
}
# Python
def quick_sort(data: list) -> list:
    """クイックソート"""
    if len(data) <= 1:
        return data
    # 実装省略
    return sorted(data)  # 簡略化

def merge_sort(data: list) -> list:
    """マージソート"""
    # 実装省略
    return sorted(data)  # 簡略化

class Sorter:
    def __init__(self, strategy=quick_sort):
        self.strategy = strategy
    
    def sort(self, data: list) -> list:
        return self.strategy(data)

2. データ圧縮方式の選択

import gzip
import bz2
import lzma

class CompressionStrategy:
    def compress(self, data: bytes) -> bytes:
        raise NotImplementedError
    
    def decompress(self, data: bytes) -> bytes:
        raise NotImplementedError

class GzipCompression(CompressionStrategy):
    def compress(self, data: bytes) -> bytes:
        return gzip.compress(data)
    
    def decompress(self, data: bytes) -> bytes:
        return gzip.decompress(data)

class FileProcessor:
    def __init__(self, compression_strategy: CompressionStrategy):
        self.compression = compression_strategy
    
    def save_compressed(self, data: str, filename: str):
        compressed = self.compression.compress(data.encode())
        with open(filename, 'wb') as f:
            f.write(compressed)

⚖️ 比較とベストプラクティス

言語別特徴まとめ

特性 Java Python
実装スタイル インターフェース + クラス 関数 or クラス(選択可能)
型安全性 コンパイル時チェック 実行時チェック(型ヒント推奨)
コード量 やや多い(厳密な設計) 少ない(柔軟な設計)
パフォーマンス 高速 やや低速(動的特性による)
拡張性 明示的な継承/実装 柔軟な追加

使い分けの指針

Strategyパターンを使うべき場面:

  • 同じ目的で複数のアルゴリズムが存在する
  • 実行時にアルゴリズムを切り替える必要がある
  • if-else文やswitch文が複雑になっている
  • 新しいアルゴリズムを頻繁に追加する可能性がある

注意点:

  • ストラテジーの数が少ない場合は、単純なif-else文の方が適切な場合もある
  • ストラテジー間でデータ共有が必要な場合は、設計を慎重に検討する
  • パフォーマンスが重要な場合は、ストラテジー切り替えのオーバーヘッドを考慮する

🎯 まとめ

Strategyパターンの本質は**「アルゴリズムのカプセル化と動的な切り替え」**です。

主な利点

  • 単一責任原則: 各アルゴリズムが独立している
  • オープン・クローズドの原則: 拡張に開いて変更に閉じている
  • 実行時の柔軟性: 動的なアルゴリズム切り替えが可能

実装のポイント

  • Java: 厳格なインターフェース設計で安全性を確保
  • Python: 関数の第一級オブジェクト特性を活用してシンプルに

両言語とも実装スタイルは異なりますが、核となる**「アルゴリズムの分離と交換可能性」**という考え方は共通です。適切な場面でこのパターンを活用することで、保守性と拡張性に優れたソフトウェアを構築できます。


次回のテーマは、「Day 24 Template Methodパターン:アルゴリズムの骨格を定義する」です。お楽しみに!

🔗 関連リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?