Strategy to jeden z tych wzorców, które po pierwszym kontakcie wydają się zbędną komplikacją. Po co opakowywać jeden algorytm w osobną klasę? Sens widać dopiero przy drugim, trzecim algorytmie – gdy zamiast rosnącego if-elsa masz czysty, wymienialny kod. Pokazuję jak to działa w PHP i gdzie Magento 2 stosuje ten wzorzec bez Twojej wiedzy.
Problem bez wzorca
Wyobraź sobie klasę kalkulatora kosztów wysyłki. Zaczynało się niewinnie:
<?php
class ShippingCalculator
{
public function calculate(string $method, float $weight): float
{
if ($method === 'flat') {
return 9.99;
} elseif ($method === 'weight') {
return $weight * 2.5;
} elseif ($method === 'free') {
return 0.0;
}
throw new \InvalidArgumentException('Unknown method: ' . $method);
}
}
Każda nowa metoda wysyłki to kolejny elseif. Testy jednostkowe tej klasy testują wszystkie algorytmy naraz. Nie da się podmienić jednego algorytmu bez ryzyka zepsucia pozostałych. Tu wchodzi Strategy.
Rozwiązanie – wzorzec Strategy
Wzorzec definiuje interfejs (kontrakt) i przenosi każdy algorytm do osobnej klasy:
<?php
// Wspólny interfejs dla wszystkich strategii
interface ShippingStrategyInterface
{
public function calculate(float $weight): float;
}
// Konkretne strategie
class FlatRateStrategy implements ShippingStrategyInterface
{
public function calculate(float $weight): float
{
return 9.99;
}
}
class WeightBasedStrategy implements ShippingStrategyInterface
{
private float $ratePerKg;
public function __construct(float $ratePerKg = 2.5)
{
$this->ratePerKg = $ratePerKg;
}
public function calculate(float $weight): float
{
return $weight * $this->ratePerKg;
}
}
class FreeShippingStrategy implements ShippingStrategyInterface
{
public function calculate(float $weight): float
{
return 0.0;
}
}
Klasa kontekstu (Context) przyjmuje strategię z zewnątrz i nie wie nic o jej implementacji:
<?php
class ShippingCalculator
{
private ShippingStrategyInterface $strategy;
public function __construct(ShippingStrategyInterface $strategy)
{
$this->strategy = $strategy;
}
public function setStrategy(ShippingStrategyInterface $strategy): void
{
$this->strategy = $strategy;
}
public function calculate(float $weight): float
{
return $this->strategy->calculate($weight);
}
}
// Użycie
$calculator = new ShippingCalculator(new WeightBasedStrategy(3.0));
echo $calculator->calculate(5.0); // 15.0
$calculator->setStrategy(new FreeShippingStrategy());
echo $calculator->calculate(5.0); // 0.0
Korzyści w praktyce
- Każda strategia to osobna klasa – testujesz ją w izolacji
- Nowa metoda wysyłki to nowa klasa, zero zmian w istniejącym kodzie (zasada Open/Closed)
- Strategię możesz wstrzyknąć przez DI – łatwa podmiana w testach i konfiguracji
Strategy w Magento 2
Magento 2 używa tego wzorca m.in. w kalkulacji cen. Interfejs Magento\Framework\Pricing\Price\PriceInterface definiuje kontrakt, a konkretne klasy jak RegularPrice, SpecialPrice, TierPrice implementują go niezależnie. Możesz dorzucić własną implementację przez DI bez dotykania istniejących klas.
Podobny mechanizm znajdziesz w sortowaniu wyników wyszukiwania (Magento\Catalog\Model\Config) czy w resolverach URL – każdy typ encji ma swoją strategię rozwiązywania adresów.
Podsumowanie
Strategy sprawdza się zawsze gdy masz zestaw wymiennych algorytmów robiących „to samo, ale inaczej”. Zamiast if-elsa rosnącego z każdym wymaganiem, dostajesz zestaw małych, testowalnych klas z jasno zdefiniowanym kontraktem. W przyszłych wpisach pokażę kolejne wzorce GoF – następny będzie Observer, który w Magento 2 ma wyjątkowo ciekawą implementację przez system zdarzeń.
