Prototype Pattern: Abstract Factory erweitert
· Lesezeit: ca. 11 Minuten · Kategorie: Magento 2 · Design Patterns
Prototype Pattern:
Abstract Factory erweitert
Wenn neue Objekte teuer zu initialisieren sind, kopiert das Prototype Pattern einen vorhandenen Prototyp. Wie Magento clone() und Factory::create() nutzt – und wann du es selbst anwenden solltest.
Kopieren statt neu erstellen
Das Prototype Pattern (GoF, 1994) löst ein spezifisches Problem: Manchmal ist das Erstellen eines neuen Objekts teuer – aufwendige Initialisierung, komplexe Abhängigkeiten, Datenbankabfragen im Constructor. In solchen Fällen ist es effizienter, ein bereits vorhandenes Objekt zu klonen und den Klon anzupassen, statt jedes Mal von vorne anzufangen.
In PHP ist das primäre Werkzeug dafür clone $object. Magento nutzt das Prototype Pattern an mehreren Stellen – manchmal explizit mit clone, häufiger implizit durch seine Factory-Infrastruktur.
- 1. Das GoF Prototype Pattern
- 2. PHP clone: Shallow vs. Deep Copy
- 3. Magento Factory: Prototype unter der Haube
- 4. DataObject klonen: Magento-Datentransporter
- 5. Shipping Rate Cloning: Konkretes Beispiel
- 6. Eigenes Prototype Pattern implementieren
- 7. Wann Prototype Pattern sinnvoll ist
- 8. Prototype vs. Factory: Der Unterschied
- 9. Prototype Pattern testen
- 10. Zusammenfassung
- 11. FAQ
1. Das GoF Prototype Pattern
Das Prototype Pattern definiert: Erstelle neue Objekte durch Kopieren (Klonen) eines Prototyp-Objekts anstatt durch direktes Instanziieren einer Klasse:
<?php
// Klassisches Prototype Pattern:
interface Prototype
{
public function clone(): static;
}
class ExpensiveObject implements Prototype
{
private array $expensiveData;
public function __construct()
{
// Teurer Constructor: lädt aus DB, initialisiert komplexe Strukturen
$this->expensiveData = $this->loadFromDatabase();
}
private function loadFromDatabase(): array
{
sleep(1); // Simuliert teuren DB-Aufruf
return ['key' => 'value', /* ... 1000 Einträge ... */];
}
public function clone(): static
{
// PHP-clone kopiert das Objekt ohne Constructor auszuführen
return clone $this;
}
}
// Ohne Prototype: Jedes new ExpensiveObject() = 1 Sekunde Wartezeit
$obj1 = new ExpensiveObject(); // 1 Sekunde
$obj2 = new ExpensiveObject(); // 1 Sekunde
$obj3 = new ExpensiveObject(); // 1 Sekunde
// Mit Prototype: Nur der erste Aufruf ist teuer
$prototype = new ExpensiveObject(); // 1 Sekunde
$obj1 = $prototype->clone(); // Instant (kein Constructor)
$obj2 = $prototype->clone(); // Instant
$obj3 = $prototype->clone(); // Instant
2. PHP clone: Shallow vs. Deep Copy
PHP's clone macht standardmäßig eine Shallow Copy – primitive Werte und Strings werden kopiert, aber Objekt-Referenzen bleiben dieselben:
<?php
class Address
{
public function __construct(
public string $street,
public string $city
) {}
}
class Customer
{
public function __construct(
public string $name,
public Address $address // Objekt-Referenz!
) {}
}
$original = new Customer('Max Muster', new Address('Hauptstr. 1', 'Berlin'));
$clone = clone $original;
// Primitive werden kopiert:
$clone->name = 'Maria Muster';
echo $original->name; // 'Max Muster' ✓ — unverändert
// Objekt-Referenzen werden NICHT kopiert:
$clone->address->city = 'München';
echo $original->address->city; // 'München' ✗ — verändert!
// Shallow Copy: $original und $clone teilen dasselbe Address-Objekt
// Lösung: __clone() Methode für Deep Copy
class Customer
{
public function __construct(
public string $name,
public Address $address
) {}
public function __clone(): void
{
// Deep Copy: Address ebenfalls klonen
$this->address = clone $this->address;
}
}
$clone2 = clone $original;
$clone2->address->city = 'Hamburg';
echo $original->address->city; // 'Berlin' ✓ — jetzt unverändert
3. Magento Factory: Prototype unter der Haube
Magentos generierte Factory-Klassen sind tatsächlich eine Form des Prototype Patterns. Beim Erstellen über Factory::create() nutzt der DI-Container intern ObjectManager::create() – das ist semantisch äquivalent zu clone $prototype, aber mit DI-Container-Unterstützung:
<?php
// Generierte Factory (generated/code/Mironsoft/Blog/Model/PostFactory.php)
namespace Mironsoft\Blog\Model;
class PostFactory
{
public function __construct(
private readonly \Magento\Framework\ObjectManagerInterface $objectManager,
private readonly string $instanceName = Post::class
) {}
/**
* Create new Post instance.
* Internally: ObjectManager::create() — non-shared, new object.
* Conceptually similar to: clone $prototype (but with full DI resolution)
*/
public function create(array $data = []): Post
{
return $this->objectManager->create($this->instanceName, $data);
}
}
// Nutzung: immer Factory statt new oder clone direkt
class PostRepository
{
public function __construct(
private readonly PostFactory $postFactory
) {}
public function getById(int $id): Post
{
$post = $this->postFactory->create(); // "Klon" des Prototyps via DI
$this->postResource->load($post, $id);
return $post;
}
}
4. DataObject klonen: Magento-Datentransporter
<?php
// Magento DataObject ist ein universeller Key-Value-Container
// Häufig geklont für "Basisdaten + Variationen"
use Magento\Framework\DataObject;
// Prototyp mit Basisdaten:
$baseRateData = new DataObject([
'carrier' => 'flatrate',
'carrier_title' => 'Flat Rate',
'method_title' => 'Fixed',
'currency' => 'EUR',
]);
// Variation A: Standardlieferung
$standard = clone $baseRateData;
$standard->setData('price', 4.99);
$standard->setData('cost', 3.50);
$standard->setData('method', 'standard');
// Variation B: Expresslieferung (ohne $baseRateData neu laden)
$express = clone $baseRateData;
$express->setData('price', 14.99);
$express->setData('cost', 9.00);
$express->setData('method', 'express');
// DataObject hat kein __clone() — Shallow Copy reicht hier,
// da DataObject nur skalare Werte im Array hält.
5. Shipping Rate Cloning: Konkretes Magento-Beispiel
Im Versandkosten-System klont Magento Rate-Objekte für verschiedene Shipping-Methoden desselben Carriers:
<?php
// In Magento\Shipping\Model\Carrier\Flatrate::collectRates() (vereinfacht):
namespace Magento\OfflineShipping\Model\Carrier;
class Flatrate extends AbstractCarrier
{
public function collectRates(RateRequest $request): ?Result
{
$result = $this->_rateResultFactory->create();
// Methoden-Objekt für "flatrate_flatrate":
/** @var \Magento\Quote\Model\Quote\Address\RateResult\Method $method */
$method = $this->_rateMethodFactory->create();
$method->setCarrier($this->_code);
$method->setCarrierTitle($this->getConfigData('title'));
$method->setMethod($this->_code);
$method->setMethodTitle($this->getConfigData('name'));
$method->setPrice($this->getFinalPriceWithHandlingFee((float)$this->getConfigData('price')));
$method->setCost((float)$this->getConfigData('price'));
$result->append($method);
// Für Multi-Method-Carrier: clone base method, modify price:
// $expressMethod = clone $method;
// $expressMethod->setMethod('express');
// $expressMethod->setPrice(14.99);
// $result->append($expressMethod);
return $result;
}
}
6. Eigenes Prototype Pattern implementieren
<?php
declare(strict_types=1);
namespace Mironsoft\Email\Model;
use Magento\Framework\Mail\MessageInterface;
/**
* Email template prototype: base template that can be cloned
* for different recipients without re-parsing the template.
*/
class EmailTemplate
{
private string $parsedBody = '';
private array $baseHeaders = [];
public function __construct(
private readonly TemplateParser $parser
) {}
/**
* Initialize the prototype with base template data.
* Expensive: parses template, loads translations, etc.
*/
public function initialize(string $templateId): void
{
$this->parsedBody = $this->parser->parse($templateId); // Teuer!
$this->baseHeaders = $this->parser->getHeaders($templateId);
}
/**
* Create a new email for a specific recipient.
* Clones the prototype and customizes recipient-specific data.
* Much cheaper than calling initialize() again.
*/
public function createForRecipient(string $email, array $vars = []): static
{
$copy = clone $this; // Kein erneuter Template-Parse!
$copy->applyVariables($vars);
$copy->setRecipient($email);
return $copy;
}
private function applyVariables(array $vars): void
{
foreach ($vars as $key => $value) {
$this->parsedBody = str_replace('{ {' . $key . '} }', $value, $this->parsedBody);
}
}
private function setRecipient(string $email): void
{
$this->baseHeaders['To'] = $email;
}
public function __clone(): void
{
// parsedBody und baseHeaders sind Arrays/Strings — keine Deep Copy nötig
// Falls Objekt-Properties: hier klonen
}
}
// Nutzung: 1x initialisieren, N mal klonen
$template = new EmailTemplate($parser);
$template->initialize('newsletter_welcome'); // 1x teuer
foreach ($recipients as $recipient) {
$email = $template->createForRecipient(
email: $recipient->getEmail(),
vars: ['name' => $recipient->getName()]
);
$this->mailer->send($email);
}
7. Wann Prototype Pattern sinnvoll ist
Prototype Pattern sinnvoll wenn:
✓ Objekt-Erstellung ist teuer (DB-Queries, Template-Parsing, komplexe Berechnungen)
✓ Viele ähnliche Objekte mit leichten Unterschieden benötigt werden
✓ Klassen-Hierarchie zur Laufzeit nicht bekannt (dynamische Objekte)
✓ Objekte haben viele Konfigurationsparameter, von denen nur wenige variieren
Prototype Pattern NICHT sinnvoll wenn:
✗ Objekt-Erstellung ist billig (einfache Value Objects)
✗ Magento Factory::create() reicht (DI-Container macht es richtig)
✗ Objekte haben komplexe Objekt-Graphen (Deep Copy ist fehleranfällig)
✗ Unterschiede zwischen Instanzen sind zu groß
In Magento:
Immer Factory::create() bevorzugen statt clone direkt
clone nur wenn Factory nicht ausreicht (z.B. Rate-Variationen)
Nie clone auf Shared Objects (Singletons) — gibt zwei "Singletons"
8. Prototype vs. Factory: Der Unterschied
| Kriterium | Prototype (clone) | Factory (create) |
|---|---|---|
| Basis | Vorhandenes Objekt kopieren | Neues Objekt via DI erstellen |
| Constructor | Wird NICHT aufgerufen | Wird aufgerufen (DI) |
| Abhängigkeiten | Übernommen vom Original | Frisch injiziert vom DI-Container |
| Performance | Schneller (kein DI-Lookup) | Langsamer (aber meist ausreichend) |
| In Magento | Selten, spezifische Fälle | Standard für Non-shared Objects |
9. Prototype Pattern testen
<?php
class EmailTemplateTest extends \PHPUnit\Framework\TestCase
{
public function testCloneIsIndependent(): void
{
$parser = $this->createMock(TemplateParser::class);
$parser->method('parse')->willReturn('Hello { {name} }!');
$parser->method('getHeaders')->willReturn(['From' => 'noreply@shop.de']);
$template = new EmailTemplate($parser);
$template->initialize('test_template');
$email1 = $template->createForRecipient('alice@example.com', ['name' => 'Alice']);
$email2 = $template->createForRecipient('bob@example.com', ['name' => 'Bob']);
// Klone sind unabhängig voneinander
// Änderung an $email1 beeinflusst $email2 nicht
$this->assertNotSame($email1, $email2);
$this->assertNotSame($email1, $template); // Original unverändert
}
public function testParserCalledOnceNotForEachClone(): void
{
$parser = $this->createMock(TemplateParser::class);
// parse() soll nur EINMAL aufgerufen werden (für initialize())
$parser->expects($this->once())->method('parse')->willReturn('Template');
$parser->expects($this->once())->method('getHeaders')->willReturn([]);
$template = new EmailTemplate($parser);
$template->initialize('test_template');
// Viele Klone — parse() wird nicht erneut aufgerufen
for ($i = 0; $i < 100; $i++) {
$template->createForRecipient("user{$i}@example.com");
}
}
}
Mironsoft
Magento 2 Performance & Architektur
Performance-Optimierung mit Design Patterns?
Wir analysieren Magento-Bottlenecks und implementieren performante Lösungen mit Prototype, Object Pool und anderen Patterns – messbar, testbar, wartbar.
10. Zusammenfassung
Das Prototype Pattern ist in Magento weniger dominant als Factory oder Strategy, aber in spezifischen Szenarien wertvoll: wenn Objekte teuer zu initialisieren sind und viele ähnliche Instanzen benötigt werden. PHPs clone-Operator macht Shallow Copies – für Deep Copy implementiere __clone(). In modernem Magento-Code ist Factory::create() fast immer vorzuziehen.
Prototype Pattern – Kernpunkte
PHP clone
Shallow Copy: Primitives kopiert, Objekt-Referenzen geteilt. Für Deep Copy: __clone() implementieren und verschachtelte Objekte ebenfalls klonen.
Magento Factory
Conceptuell ähnlich, aber mit vollständiger DI-Unterstützung. Factory::create() ist die bevorzugte Methode für neue Instanzen in Magento.
Einsatz
Sinnvoll wenn: Initialisierung teuer, viele ähnliche Instanzen nötig. Nicht sinnvoll: einfache Objekte, Factory reicht aus, komplexe Objekt-Graphen.
Vorsicht
Nie Shared Objects klonen (verletzt Singleton-Semantik). Immer prüfen ob Shallow Copy ausreicht oder __clone() für tiefe Kopie nötig ist.
11. FAQ: Prototype Pattern in Magento 2
1 Wann clone statt Factory::create()?
2 Shallow Copy vs. Deep Copy?
__clone() implementieren.3 Magento-Models klonen?
4 __clone() korrekt implementieren?
$this->address = clone $this->address;. Arrays mit Objekten: foreach + clone je Element.5 Wo nutzt Magento Core clone?
6 Shared Objects klonen?
7 DataObject für Prototype Pattern?
8 Prototype-Code testen?
9 Performance: clone vs. new?
10 Prototype mit Interface definieren?
clone(): static Methode. Rückgabetyp static für Kovarianz in PHP 8+. Implementierung meist: return clone $this; mit optionaler __clone()-Logik.