Strategy Pattern: Austauschbare Algorithmen in Magento

· Lesezeit: ca. 13 Minuten · Kategorie: Magento 2 · Design Patterns

STR
algo
Magento 2 · Deep Dive · 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.

⏱ 13 Min. Deep Dive Design Patterns PHP 8.4

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

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)
ZweckGesamten Algorithmus ersetzenBestehende Methode vor/nach/um erweitern
Konfigurationdi.xml preference oder Argumentdi.xml plugin-Element
Mehrere gleichzeitigNur eine (oder Composite)Mehrere mit sortOrder
OriginalcodeWird komplett ersetztBleibt erhalten (Before/After) oder optional (Around)
EinsatzEigene Business-Logik, Extensibility-PointsVerhalten 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.

Preisstrategien
Eigene Preiskalkulatoren für Kundengruppen, Warenkorbwert, Stammkunden oder B2B-Kunden.
Versandstrategien
Custom Carrier mit eigener Preislogik: Entfernungsbasiert, gewichtsabhängig, kundenspezifisch.
Extensibility-Points
Strategy-Interfaces in eigenen Modulen definieren damit Third-Party-Module austauschbare Implementierungen liefern können.

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?
Kontext von konkreter Implementierung entkoppelt. Neue Algorithmen hinzufügen ohne Code-Änderungen (Open/Closed). Konfiguration via di.xml. Vollständige Testbarkeit durch Interface-Mocking.
2 Wie tausche ich eine Standard-Strategy?
Via di.xml preference: <preference for="..." type="MeineImplementierung"/>. Nach setup:di:compile aktiv. Kein Code-Change im Kontext nötig.
3 Mehrere Strategien gleichzeitig?
Ja — Composite Strategy: Liste von Strategies, erste anwendbare gewinnt. Liste als Array-Argument in di.xml konfigurieren mit sortOrder.
4 Strategy vs. Plugin: Wann was?
Strategy: ganzen Algorithmus ersetzen, eigene Extensibility-Points. Plugin: Verhalten einer Core-Methode erweitern ohne Original zu ersetzen.
5 Eigenen Extensibility-Point definieren?
1. Interface definieren. 2. Default-Implementierung erstellen. 3. Kontext injiziert Interface. 4. di.xml: preference auf Default. Third-Party-Module können dann eigene Implementierungen registrieren.
6 Was sind Virtual Types bei Strategy?
Konfigurationsvarianten derselben Klasse ohne neuen PHP-Code. DiscountCalculator als VipDiscountCalculator + CartDiscountCalculator — gleiche Klasse, verschiedene Strategy injiziert. Komplett in di.xml konfigurierbar.
7 Strategy-Implementierungen testen?
Sehr einfach: Context (Value Object) erstellen, Strategy instanziieren, calculate() aufrufen, Ergebnis prüfen. Kein Bootstrap, keine DB. Kontext testen mit createMock(StrategyInterface::class).
8 Eigenen Versandanbieter implementieren?
AbstractCarrier erweitern + CarrierInterface implementieren. collectRates(RateRequest) mit eigener Logik. In config.xml und system.xml konfigurieren. Magento erkennt den Carrier automatisch.
9 Strategy-Auswahl aus der Datenbank?
Ja — mit einem Strategy-Resolver: liest aus DB welche Strategy gilt, gibt entsprechende Implementierung zurück. Strategien bleiben via di.xml konfiguriert — Resolver entscheidet nur welche aktiv ist.
10 Strategy vs. Template Method Pattern?
Strategy: ganzer Algorithmus austauschbar, unabhängige Klassen, Wechsel via Injection. Template Method: Basis-Algorithmus fest in Basisklasse, Hooks für Subklassen, Wechsel via Vererbung. Magento nutzt beide: AbstractCarrier (Template Method) + CarrierInterface (Strategy).