Wenn in einem Modul so viele Arbeiter (Kindklassen) vorhanden sind, dass es aussieht wie eine Fabrik, dann könnte die Factory das Entwurfsmuster sein mit dem die Verwaltung wieder einfach wird.
Aber Spaß beiseite: In diesem Beitrag wird es darum gehen verschiedene Typen in Form von Kindklassen effizient zu instanzieren.
Die Ausgangssituation
Viele Klassen eines Interfaces
Unsere Anwendung soll verschiedenen Partnern (die uns möglicherweise auch noch gar nicht alle bekannt sind) eine Schnittstelle zur Verfügung stellen. Natürlich werden die Partner alle etwas anders zu handhaben sein – sie bekommen also alle eine eigene Klasse. Auf unserer Seite muss die Schnittstelle aber für alle das gleiche Resultat liefern, damit wir nicht mit jedem neuen Partner unseren Code anfassen müssen.
Wir erstellen also ein Interface an das sich alle Partner zu halten haben.
1 2 3 4 |
interface IsPartner { public function getApiKey(); public function getApiSecret(); } |
1 2 3 |
class Cashcow implements IsPartner { /* ... */ } class Shark implements IsPartner { /* ... */ } |
Instanzieren der Kindklasse
Stellt der Partner jetzt eine Anfrage an unsere API, benötigen wir eine Instanz „seiner“ Klasse. Da wir natürlich keine Abhängigkeiten erzeugen wollen, müssten wir diese alle in unserem Service-Container ablegen – was sehr schnell ausarten kann. Wir benötigen also eine zentrale Anlaufstelle für alle Partnerklassen: Eine Factory.
Die Factory
Zunächst ihr Code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class PartnerFactory { private $mapping; /** * Hier definieren wir die möglichen Klassen. Ein neuer Partner muss lediglich hier angemeldet werden. */ public function __construct() { $this->mapping = [ // Bezeichnung => Zugehörige Klasse 'cashcow' => 'Cashcow', 'bunny' => 'Shark', ]; } /** * Eine Instanz erstellen. * * @param string $type * * @return IsPartner */ public function get($type) { if (!isset($this->mapping[$type])) { throw new \InvalidArgumentException('Unknown type: '.$type); } // Die Factory liegt im obersten Ordner des Moduls $className = __NAMESPACE__.'\\'.$this->mapping[$type]; return new $className(); } } |
Die Factory kann jetzt wie üblich im Service-Container hinterlegt werden und mit ihr wird dann die entsprechende Partner-Klasse geladen.
1 2 3 4 5 6 |
// irgendwo im Bootprozess DI::set('partnerFactory', new PartnerFactory()); // im Schnittstellen-Controller $factory = DI::get('partnerFactory'); $partner = $factory->get($partnerName); |
Varianten
Obiges Beispiel zeigt eine Factory als eigene Klasse, alternativ dazu gibt es noch die Möglichkeit als Methode in einer abstrakten Elternklasse.
Factory-Methode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
abstract class Partner { /** * @param $type * * @return self */ public static function factory($type) { $mapping = [ // Bezeichnung => Zugehörige Klasse 'cashcow' => 'Cashcow', 'bunny' => 'Shark', ]; if (!isset($mapping[$type])) { throw new \InvalidArgumentException('Unknown type: '.$type); } // Die Factory liegt im obersten Ordner des Moduls $className = __NAMESPACE__.'\\'.$mapping[$type]; return new $className(); } } |
Diese Vorgehensweise führt zu einer statischen Methode, die in UnitTests Komplikationen nach sich zieht. Dazu kommt, dass eine abstrakte Klasse nicht im Service-Container hinterlegt werden kann. Diese Art der Factory ist also weniger geschickt als eine eigenständige Klasse.
Fazit
Mit dem Entwurfsmuster der Factory erhalten wir eine zentrale Anlaufstelle für alle Klassen einer Gruppe. Für UnitTests kann die Factory einfach simuliert werden und die Wartbarkeit des Codes ist gewährleistet.