Strategyパターン
CoddyのPHPジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 74/91。
ストラテジーパターン(Strategy Pattern)は、一連のアルゴリズムを定義し、それぞれを独自のクラスにカプセル化して、それらを実行時に相互に入れ替え可能にする振る舞いに関するデザインパターンです。ストラテジーを使用するオブジェクトは、どのアルゴリズムが実行されているかを知る必要はありません。
さまざまな割引戦略を適用できるショッピングカートを想像してみてください。条件分岐を使って割引ロジックをハードコーディングする代わりに、各割引を個別のクラスとして定義します:
<?php
interface DiscountStrategy {
public function calculate(float $total): float;
}
class NoDiscount implements DiscountStrategy {
public function calculate(float $total): float {
return $total;
}
}
class PercentageDiscount implements DiscountStrategy {
public function __construct(private int $percent) {}
public function calculate(float $total): float {
return $total - ($total * $this->percent / 100);
}
}
class FixedDiscount implements DiscountStrategy {
public function __construct(private float $amount) {}
public function calculate(float $total): float {
return max(0, $total - $this->amount);
}
}
コンテキストクラスは任意のストラテジーを受け入れ、計算を委譲します:
<?php
class ShoppingCart {
public function __construct(private DiscountStrategy $discount) {}
public function setDiscount(DiscountStrategy $discount): void {
$this->discount = $discount;
}
public function checkout(float $total): float {
return $this->discount->calculate($total);
}
}
$cart = new ShoppingCart(new NoDiscount());
echo $cart->checkout(100) . "\n";
$cart->setDiscount(new PercentageDiscount(20));
echo $cart->checkout(100);
出力:
100
80ストラテジーパターンは、ある操作を実行するための複数の方法があり、それらを使用するクラスを修正することなく方法を切り替えたい場合に本領を発揮します。新しい割引タイプを追加するということは、既存のコードに手を加えるのではなく、新しいクラスを作成することを意味します。
チャレンジ
簡単Strategyパターンを使用して、送料計算機を構築しましょう。配送方法によって価格設定ルールが異なります。標準配送(standard)は一律料金、速達配送(express)は重量に応じた加算、翌日配送(overnight)はその両方を組み合わせたものです。Strategyパターンを使用すると、それらを利用するコードを変更することなく、これらの計算ロジックを入れ替えることができます。
コードは4つのファイルに分けて構成します:
ShippingStrategy.php— すべての配送方法のコントラクトを定義するShippingStrategyインターフェースを作成します。これには、パッケージの重量を受け取り送料を返すcalculate(float $weight): floatメソッドが必要です。ShippingMethods.php— ShippingStrategyファイルを読み込み、インターフェースを実装する3つのクラスを作成します:StandardShipping— 重量に関わらず一律$5.00を請求しますExpressShipping— 1キログラムあたり$2.00を請求します(重量に2を掛けます)OvernightShipping— 基本料金$10.00に加え、1キログラムあたり$3.00を請求します
ShippingCalculator.php— ShippingStrategyファイルを読み込み、コンテキストとして機能するShippingCalculatorクラスを作成します。この計算機は以下の機能を持つ必要があります:- コンストラクタで
ShippingStrategyを受け取る(コンストラクタのプロパティ昇格を使用してください) - 実行時に配送方法を変更するための
setStrategy(ShippingStrategy $strategy): voidメソッドを持つ - 現在のストラテジーに処理を委譲する
calculateCost(float $weight): floatメソッドを持つ
- コンストラクタで
main.php— ShippingMethodsとShippingCalculatorファイルを読み込みます。2つの入力、配送タイプ("standard"、"express"、または"overnight")とパッケージの重量を受け取ります。最初に
StandardShippingを指定してShippingCalculatorを作成します。入力された配送タイプに基づいて、setStrategy()を使用して適切な配送方法に切り替えます。次に、指定された重量のコストを計算し、結果を小数点以下2桁にフォーマットして出力します。出力形式は次の通りです:
Shipping cost: $[cost]
このチャレンジは、Strategyパターンがいかに配送システムを柔軟にするかを示しています。SameDayShipping のような新しい配送方法を追加する場合でも、計算機や既存の配送方法に手を加えることなく、インターフェースを実装する新しいクラスを作成するだけで済みます。
チートシート
ストラテジーパターン(Strategy Pattern)は、アルゴリズムの集合を定義し、それぞれを独自のクラスにカプセル化して、実行時にそれらを入れ替え可能にする振る舞いに関するデザインパターンです。
ストラテジーのインターフェースを定義します:
<?php
interface DiscountStrategy {
public function calculate(float $total): float;
}具体的なストラテジーを実装します:
<?php
class NoDiscount implements DiscountStrategy {
public function calculate(float $total): float {
return $total;
}
}
class PercentageDiscount implements DiscountStrategy {
public function __construct(private int $percent) {}
public function calculate(float $total): float {
return $total - ($total * $this->percent / 100);
}
}
class FixedDiscount implements DiscountStrategy {
public function __construct(private float $amount) {}
public function calculate(float $total): float {
return max(0, $total - $this->amount);
}
}ストラテジーを使用するコンテキストクラスを作成します:
<?php
class ShoppingCart {
public function __construct(private DiscountStrategy $discount) {}
public function setDiscount(DiscountStrategy $discount): void {
$this->discount = $discount;
}
public function checkout(float $total): float {
return $this->discount->calculate($total);
}
}ストラテジーを注入してパターンを使用します:
<?php
$cart = new ShoppingCart(new NoDiscount());
echo $cart->checkout(100); // 100
$cart->setDiscount(new PercentageDiscount(20));
echo $cart->checkout(100); // 80ストラテジーパターンを使用すると、アルゴリズムを使用するクラスを変更することなく、アルゴリズムを切り替えることができます。新しいストラテジーを追加するには、インターフェースを実装する新しいクラスを作成するだけです。
自分で試してみよう
<?php
require_once 'ShippingMethods.php';
require_once 'ShippingCalculator.php';
// 入力を読み込む
$shippingType = trim(fgets(STDIN));
$weight = floatval(fgets(STDIN));
// TODO: StandardShipping を初期値として ShippingCalculator を作成する
// TODO: 入力された配送タイプ("standard"、"express"、または "overnight")に基づいて、
// setStrategy() を使用して適切な配送方法に切り替える
// TODO: 指定された重量のコストを計算する
// TODO: 小数点以下2桁にフォーマットして結果を出力する
// 出力形式: Shipping cost: $[cost]
?>このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。