Strategy Pattern: Austauschbare Algorithmen in Magento
· Lesezeit: ca. 13 Minuten · Kategorie: Magento 2 · Design Patterns
Strategy Pattern:
Austauschbare Algorithmen
Preisberechnung, Versandkosten, Steuern, Zahlungsarten – Magento nutzt Strategy Pattern überall. Wie es implementiert wird, wie di.xml Algorithmen tauscht und wie du eigene Strategien integrierst.
Wenn Algorithmen austauschbar sein müssen
Wie berechnet Magento den Preis eines Produkts? Das hängt ab: Hat der Kunde eine Kundengruppe mit Rabatt? Gibt es eine aktive Preisregel? Ist der Kunde in einem Steuerkollektiv? Welche Währung ist aktiv? Die Antwort ist jedes Mal anders – und genau dafür ist das Strategy Pattern ideal.
Das Strategy Pattern (GoF, 1994) definiert eine Familie von Algorithmen, kapselt jeden einzeln und macht sie austauschbar. Der Kontext (die aufrufende Klasse) kennt nur das Interface – welche konkrete Implementierung ausgeführt wird, entscheidet die Konfiguration.
Magento nutzt dieses Pattern massiv: Versandkostenberechnung (CarrierInterface), Zahlungsarten (MethodInterface), Steuerberechnungen, Preiskalkulatoren und viele andere austauschbare Algorithmen sind alle Strategy-Implementierungen.
- 1. Das GoF Strategy Pattern
- 2. Strategy Pattern in Magento Core: Überblick
- 3. Versandkosten: CarrierInterface als Strategy
- 4. Preisberechnung: PriceInterface Strategies
- 5. Eigene Strategy implementieren
- 6. di.xml: Strategien konfigurieren und tauschen
- 7. Strategy Chain: Mehrere Strategien kombinieren
- 8. Strategy Pattern testen
- 9. Strategy vs. Plugin: Wann welches Pattern?
- 10. Zusammenfassung
- 11. FAQ
1. Das GoF Strategy Pattern
Das klassische Strategy Pattern besteht aus drei Teilen: Strategy Interface, konkreten Implementierungen und einem Kontext, der eine Strategy nutzt:
<?php
// Klassisches Strategy Pattern:
// 1. Strategy Interface
interface SortStrategyInterface
{
public function sort(array $data): array;
}
// 2. Konkrete Strategien
class QuickSort implements SortStrategyInterface
{
public function sort(array $data): array { /* QuickSort */ return $data; }
}
class MergeSort implements SortStrategyInterface
{
public function sort(array $data): array { /* MergeSort */ return $data; }
}
class BubbleSort implements SortStrategyInterface
{
public function sort(array $data): array { /* BubbleSort */ return $data; }
}
// 3. Kontext: kennt nur das Interface
class DataSorter
{
public function __construct(
private readonly SortStrategyInterface $strategy // Austauschbar!
) {}
public function sort(array $data): array
{
return $this->strategy->sort($data); // Welche Implementierung? Egal!
}
}
// Konfiguration entscheidet welche Strategy:
$sorter = new DataSorter(new QuickSort());
$sorter = new DataSorter(new MergeSort()); // Kein Code-Change im Kontext!
2. Strategy Pattern in Magento Core: Überblick
Strategy Pattern in Magento Core:
Versandkosten:
Interface: Magento\Shipping\Model\Carrier\AbstractCarrier
Strategien: FlatRate, FreeShipping, TableRate, UPS, DHL, FedEx
Kontext: Magento\Shipping\Model\Shipping
Zahlungsarten:
Interface: Magento\Payment\Model\Method\AbstractMethod
(+ Magento\Payment\Gateway\CommandInterface)
Strategien: CashOnDelivery, BankTransfer, Stripe, PayPal, Klarna
Kontext: Magento\Payment\Helper\Data
Preiskalkulatoren:
Interface: Magento\Catalog\Pricing\Price\PriceInterface
Strategien: RegularPrice, SpecialPrice, GroupPrice, TierPrice
Kontext: Magento\Framework\Pricing\Amount\AmountFactory
Steuerberechnung:
Interface: Magento\Tax\Model\Calculation\AbstractAggregateCalculator
Strategien: UnitBaseCalculation, RowBaseCalculation, TotalBaseCalculation
Kontext: Magento\Tax\Model\Calculation
URL-Generierung:
Interface: Magento\Catalog\Model\ResourceModel\Url
Strategien: je nach Produkt-Typ unterschiedliche URL-Builder
Produkt-Typen:
Interface: Magento\Catalog\Model\Product\Type\AbstractType
Strategien: Simple, Configurable, Bundle, Virtual, Downloadable
3. Versandkosten: CarrierInterface als Strategy
Das deutlichste Beispiel: Jede Versandart ist eine eigenständige Strategy. Sie teilen dasselbe Interface, haben aber komplett unterschiedliche Berechnungslogiken:
<?php
// Magento\Shipping\Model\CarrierInterface (vereinfacht):
interface CarrierInterface
{
/**
* Collect and get rates.
* Each carrier implements its own rate-calculation algorithm.
*/
public function collectRates(RateRequest $request): ?Result;
/**
* Returns allowed shipping methods for this carrier.
*/
public function getAllowedMethods(): array;
}
// Konkrete Strategien:
class Flatrate extends AbstractCarrier implements CarrierInterface
{
public function collectRates(RateRequest $request): ?Result
{
// Immer gleicher Preis aus Konfiguration
$price = $this->getConfigData('price');
$method = $this->_rateMethodFactory->create();
$method->setPrice($price);
// ...
return $result;
}
}
class Tablerate extends AbstractCarrier implements CarrierInterface
{
public function collectRates(RateRequest $request): ?Result
{
// Preis aus Tabelle basierend auf Gewicht/Preis/Destination
$rate = $this->_tablerateFactory->create()
->getRate($request);
// ...
return $result;
}
}
// Kontext: Shipping-Klasse kennt nur das Interface
class Shipping
{
public function collectCarrierRates(string $carrierCode, RateRequest $request): ?Result
{
$carrier = $this->_carrierFactory->create($carrierCode);
if (!$carrier) {
return null;
}
// Ruft collectRates() auf — welche Implementierung? Egal!
return $carrier->collectRates($request);
}
}
4. Preisberechnung: PriceInterface Strategies
<?php
// Magento\Framework\Pricing\Price\PriceInterface:
interface PriceInterface
{
/**
* Get price value: each price type has its own calculation.
*/
public function getValue(): float|false;
/**
* Get the price amount object with adjustments.
*/
public function getAmount(): AmountInterface;
}
// Preisstrategien:
class RegularPrice implements PriceInterface
{
public function getValue(): float|false
{
return $this->product->getPrice(); // Direkter Katalogpreis
}
}
class SpecialPrice implements PriceInterface
{
public function getValue(): float|false
{
$specialPrice = $this->product->getSpecialPrice();
if ($specialPrice === null) {
return false;
}
// Prüft Gültigkeit (from/to date)
if ($this->isDateInRange($this->product->getSpecialFromDate(), $this->product->getSpecialToDate())) {
return (float) $specialPrice;
}
return false;
}
}
class TierPrice implements PriceInterface
{
public function getValue(): float|false
{
// Günstigster Tier-Preis für aktuelle Kundengruppe
$tierPrices = $this->product->getTierPrices();
// ... Berechnung basierend auf Menge und Kundengruppe
return $lowestTierPrice;
}
}
5. Eigene Strategy implementieren
Ein konkretes Beispiel: Eigene Rabatt-Strategie für verschiedene Kundengruppen:
<?php
declare(strict_types=1);
namespace Mironsoft\Pricing\Model\Discount;
// 1. Strategy Interface definieren
interface DiscountStrategyInterface
{
/**
* Calculate discount amount for a given price and context.
*/
public function calculate(float $price, DiscountContext $context): float;
/**
* Returns true if this strategy applies for the given context.
*/
public function isApplicable(DiscountContext $context): bool;
}
// 2. Konkreter Context (Value Object)
class DiscountContext
{
public function __construct(
public readonly int $customerGroupId,
public readonly float $cartTotal,
public readonly \DateTimeImmutable $orderDate
) {}
}
// 3. Strategie A: Kundengruppen-Rabatt
class CustomerGroupDiscount implements DiscountStrategyInterface
{
private const VIP_GROUP_ID = 4;
private const VIP_DISCOUNT = 0.15; // 15%
public function calculate(float $price, DiscountContext $context): float
{
return $price * self::VIP_DISCOUNT;
}
public function isApplicable(DiscountContext $context): bool
{
return $context->customerGroupId === self::VIP_GROUP_ID;
}
}
// 4. Strategie B: Warenkorbwert-Rabatt
class CartValueDiscount implements DiscountStrategyInterface
{
public function calculate(float $price, DiscountContext $context): float
{
return match(true) {
$context->cartTotal >= 500.0 => $price * 0.10,
$context->cartTotal >= 200.0 => $price * 0.05,
default => 0.0,
};
}
public function isApplicable(DiscountContext $context): bool
{
return $context->cartTotal >= 200.0;
}
}
// 5. Kontext-Klasse: kennt nur das Interface
class DiscountCalculator
{
public function __construct(
private readonly DiscountStrategyInterface $strategy // Austauschbar!
) {}
public function getDiscount(float $price, DiscountContext $context): float
{
if (!$this->strategy->isApplicable($context)) {
return 0.0;
}
return $this->strategy->calculate($price, $context);
}
}
6. di.xml: Strategien konfigurieren und tauschen
Die Stärke des Strategy Patterns in Magento: Die Strategy wird via di.xml konfiguriert. Keine Code-Änderung am Kontext nötig – nur Konfigurationsänderung:
<!-- app/code/Mironsoft/Pricing/etc/di.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<!-- Standard-Strategy: CustomerGroupDiscount -->
<preference for="Mironsoft\Pricing\Model\Discount\DiscountStrategyInterface"
type="Mironsoft\Pricing\Model\Discount\CustomerGroupDiscount"/>
<!-- Alternative: CartValueDiscount statt CustomerGroupDiscount -->
<!-- Auskommentieren zum Tauschen: -->
<!--
<preference for="Mironsoft\Pricing\Model\Discount\DiscountStrategyInterface"
type="Mironsoft\Pricing\Model\Discount\CartValueDiscount"/>
-->
<!-- Für spezifische Klasse andere Strategy (Virtual Type Pattern): -->
<virtualType name="Mironsoft\Pricing\Model\VipDiscountCalculator"
type="Mironsoft\Pricing\Model\Discount\DiscountCalculator">
<arguments>
<argument name="strategy" xsi:type="object">
Mironsoft\Pricing\Model\Discount\CustomerGroupDiscount
</argument>
</arguments>
</virtualType>
<virtualType name="Mironsoft\Pricing\Model\CartDiscountCalculator"
type="Mironsoft\Pricing\Model\Discount\DiscountCalculator">
<arguments>
<argument name="strategy" xsi:type="object">
Mironsoft\Pricing\Model\Discount\CartValueDiscount
</argument>
</arguments>
</virtualType>
</config>
Virtual Types: Mit Virtual Types kann derselbe Kontext (DiscountCalculator) mit verschiedenen Strategien mehrfach existieren, ohne neue PHP-Klassen schreiben zu müssen. VipDiscountCalculator und CartDiscountCalculator sind dieselbe Klasse – nur mit unterschiedlicher Strategy injiziert.
7. Strategy Chain: Mehrere Strategien kombinieren
<?php
declare(strict_types=1);
namespace Mironsoft\Pricing\Model\Discount;
/**
* Composite strategy: tries multiple strategies, applies the first that matches.
* Implements the Chain of Responsibility variant of Strategy Pattern.
*/
class CompositeDiscountStrategy implements DiscountStrategyInterface
{
/**
* @param DiscountStrategyInterface[] $strategies Ordered list of strategies
*/
public function __construct(
private readonly array $strategies = []
) {}
public function calculate(float $price, DiscountContext $context): float
{
foreach ($this->strategies as $strategy) {
if ($strategy->isApplicable($context)) {
return $strategy->calculate($price, $context);
}
}
return 0.0;
}
public function isApplicable(DiscountContext $context): bool
{
foreach ($this->strategies as $strategy) {
if ($strategy->isApplicable($context)) {
return true;
}
}
return false;
}
}
<!-- Composite Strategy via di.xml konfigurieren: -->
<type name="Mironsoft\Pricing\Model\Discount\CompositeDiscountStrategy">
<arguments>
<argument name="strategies" xsi:type="array">
<!-- sortOrder bestimmt Priorität (erst VIP, dann Warenkorbwert) -->
<item name="vip" xsi:type="object" sortOrder="10">
Mironsoft\Pricing\Model\Discount\CustomerGroupDiscount
</item>
<item name="cart" xsi:type="object" sortOrder="20">
Mironsoft\Pricing\Model\Discount\CartValueDiscount
</item>
</argument>
</arguments>
</type>
8. Strategy Pattern testen
<?php
declare(strict_types=1);
use Mironsoft\Pricing\Model\Discount\CustomerGroupDiscount;
use Mironsoft\Pricing\Model\Discount\DiscountContext;
use Mironsoft\Pricing\Model\Discount\DiscountCalculator;
class CustomerGroupDiscountTest extends \PHPUnit\Framework\TestCase
{
private CustomerGroupDiscount $strategy;
protected function setUp(): void
{
$this->strategy = new CustomerGroupDiscount();
}
public function testCalculateVipDiscount(): void
{
$context = new DiscountContext(
customerGroupId: 4, // VIP-Gruppe
cartTotal: 100.0,
orderDate: new \DateTimeImmutable()
);
$discount = $this->strategy->calculate(100.0, $context);
$this->assertSame(15.0, $discount); // 15% von 100 = 15
}
public function testNotApplicableForNonVip(): void
{
$context = new DiscountContext(customerGroupId: 1, cartTotal: 0.0, orderDate: new \DateTimeImmutable());
$this->assertFalse($this->strategy->isApplicable($context));
}
}
class DiscountCalculatorTest extends \PHPUnit\Framework\TestCase
{
public function testUsesStrategy(): void
{
$context = new DiscountContext(customerGroupId: 4, cartTotal: 100.0, orderDate: new \DateTimeImmutable());
$strategy = $this->createMock(\Mironsoft\Pricing\Model\Discount\DiscountStrategyInterface::class);
$strategy->method('isApplicable')->with($context)->willReturn(true);
$strategy->method('calculate')->with(100.0, $context)->willReturn(20.0);
$calculator = new DiscountCalculator($strategy);
$this->assertSame(20.0, $calculator->getDiscount(100.0, $context));
}
}
9. Strategy vs. Plugin: Wann welches Pattern?
| Kriterium | Strategy Pattern | Plugin (Interceptor) |
|---|---|---|
| Zweck | Gesamten Algorithmus ersetzen | Bestehende Methode vor/nach/um erweitern |
| Konfiguration | di.xml preference oder Argument | di.xml plugin-Element |
| Mehrere gleichzeitig | Nur eine (oder Composite) | Mehrere mit sortOrder |
| Originalcode | Wird komplett ersetzt | Bleibt erhalten (Before/After) oder optional (Around) |
| Einsatz | Eigene Business-Logik, Extensibility-Points | Verhalten von Core-Klassen modifizieren |
Mironsoft
Magento 2 Architektur & Entwicklung
Eigene Strategien für dein Magento entwickeln?
Wir implementieren austauschbare Algorithmen für Preisberechnung, Versandkosten, Steuerregeln und mehr – mit Strategy Pattern, vollständiger Testabdeckung und di.xml-Konfiguration.
10. Zusammenfassung
Das Strategy Pattern ist in Magento 2 das am häufigsten verwendete Design Pattern. Es ermöglicht austauschbare Algorithmen für Preisberechnung, Versandkosten, Zahlungsarten und mehr. Das Interface definiert den Vertrag, di.xml entscheidet welche Implementierung aktiv ist – und der aufrufende Kontext kennt keine konkreten Klassen.
Strategy Pattern in Magento – Überblick
Interface
Definiert den Algorithmus-Vertrag. Kontext kennt nur das Interface. Alle Strategien implementieren dasselbe Interface.
di.xml Konfiguration
preference oder argument wählt die konkrete Implementierung. Tauschen = Konfigurationsänderung, kein Code-Change im Kontext.
Virtual Types
Derselbe Kontext mit verschiedenen Strategien als verschiedene virtuelle Klassen — keine neuen PHP-Klassen nötig.
Composite Strategy
Mehrere Strategien in einer Composite-Strategie kombinieren. Reihenfolge via sortOrder in di.xml konfigurierbar.
11. FAQ: Strategy Pattern in Magento 2
1 Hauptvorteil des Strategy Patterns in Magento?
2 Wie tausche ich eine Standard-Strategy?
di.xml preference: <preference for="..." type="MeineImplementierung"/>. Nach setup:di:compile aktiv. Kein Code-Change im Kontext nötig.