Facebook
Twitter
Google+
Kommentare
19

PHP-Entwurfsmuster: Factory Method

Auf heutigen Tag widmen wir uns mal wieder einem Entwurfsmuster. Um ganz genau zu seine einem Erzeugungsmuster, also eine Muster, dass sich darum kümmern Klassen zu instanzieren. So oft „Muster“ in einem so kurzen Abschnitt … Respekt! Natürlich kann man jedes Objekt mit „new“ erstellen und fertig. Aber das wäre uns ja zu einfach. Naja vielleicht nicht zu einfach, man sollte weiterhin new verwenden, wenn es Sinn macht. Manchmal gibt es aber saubere Methoden.

Nehmen wir uns einfach mal mein Lieblingsbeispiel: den Logger. Wir haben eine Anwendung geschrieben, die einen Logger benötigt, um alle wichtigen Dinge in eine Textdatei zu schreiben. Sollte ja jedem vertraut klingen. Um jetzt nicht wild mit perfekten Beispielen rumzuwerfen nehmen wir einfach was sehr simples.

<?php
class FileLogger implements Logger
{
  // ... 

  public function log( $message )
  {
    // ...
  }
}

$logger = new FileLogger( 'log.txt' );

// ...

$logger->log( 'Something important has happend' );

// ...

?>

Bis jetzt scheint der Code sauber. Denn wir hantieren mit Interfaces und nutzen den Logger auch wie es sein soll. Das Problem ist nur, dass wir vielleicht den Logger irgendwann austauschen wollen. In Textfiles loggen ist ja so 80er. Wir speichern jetzt alles in eine Datenbank. Eigentlich wäre es doch aber toll, wenn man sowas als eine Art Konfiguration sehen und nicht als Codeanpassung. Vielleicht will ich ja auch mal wieder zurückschwenken oder im Dev-System auf einen EchoLogger zurückgreifen. Das kann in mit einem solchen Code grad mal vergessen.

Wir müssen also verhindern gegen eine konkrete Implementierung zu entwickeln, sondern müssen das ein wenig abstrahieren. Natürlich hilft da das Entwurfsmuster, mit dem wir heute ein wenig rumspielen. Wie vorhin schon angedeutet, werden wir nicht mit new hantieren, sondern die Instanz über eine Fabrik-Methode erstellen. Ich zeig am besten erst mal, wie es aussehen könnte und dann erkläre ich, warum es so aussieht.

<?php

public function loggerFactory( $loggerType, $params )
{
  switch $loggerType
  {
    case 'text':
      // ein wenig defensives programmieren hier ...
      $logger = new TextLogger( $params['filename' );
     break;
     case 'db':
       // ein wenig defensives programmieren hier ...
       $logger = new DbLogger( $params['database_name' /* ... */   );
       break;
     default:
       throw new Exception( 'Logger not found' );
   }
  return $logger;
}

Jetzt können wir uns ganz einfach einen Logger bauen:

<?php
  $config = array( 'logger_type' => 'text', 'filename' => 'log.txt' );
  $logger = loggerFactory( $config['logger_type'], $config );
  /* @var $logger Logger */
?>

Woher denn jetzt genau diese Config-Datei kommt, da wollen wir gar nicht drauf eingehen.Wichtig ist nur, dass wir jetzt Konfigurieren und nicht mehr eine konkrete Klasse instanzieren. Unser Code ist jetzt also vorbereitet, diese Konfiguration außerhalb des Source-Codes zu betreiben und damit haben wir gewonnen. Meiner Meinung nach haben wir sogar doppelt gewonnen, denn wir haben uns auch noch dazu genötigt, mit Interfaces zu arbeiten, was ja leider in PHP nicht immer an der Tagesordnung ist.

Über den Autor

Nils Langner

Nils Langner ist der Gründer von "the web hates me" und auch der Hauptautor. Im wahren Leben leitet er das Qualitätsmanagementteam im Gruner+Jahr-Digitalbereich und ist somit für Seiten wie stern.de, eltern.de und gala.de aus Qualitätssicht verantwortlich. Nils schreibt seit den Anfängen von phphatesme, welches er ebenfalls gegründet hat, nicht nur für diverse Blogs, sondern auch für Fachmagazine, wie das PHP Magazin, die t3n, die c't oder die iX. Nebenbei ist er noch ein gern gesehener Sprecher auf Konferenzen. Herr Langner schreibt die Texte über sich gerne in der dritten Form.
Kommentare

19 Comments

  1. Hm.. Ich muss gestehen, dass ich bis heute zwar das Prinzip hinter dem Factory Pattern, aber nicht seinen Sinn verstanden habe. Irgendwie kam mir nie in den Sinn, dass sich dieses Pattern mit der Konfiguration verbinden lässt und somit zum Beispiel dieses Problem der verschiedenen Logger in den Environments vereinfacht. Ich glaube, mein Bootstrap wird dadurch kürzer^^

    Eine Frage habe ich allerdings: Spricht etwas dagegen, die Factory annehmen zu lassen, dass Logger immer Logger_File o.ä. heißen? Wenn man einen Autoloader benutzt muss man schließlich somit nie den Code der Factory anpassen sondern kann einfach eine neue Datei anlegen. Oder gibt es etwas, was dagegen spricht?

    Reply
  2. *mit leids kommentar* (nach twitter aufruf) 😉

    Klingt interessant, leider ist das Logger Beispiel nach dem Buch von Stephan Schmidt schon ziemlich … (wie nenn ich es mal?) „ausgelutscht“. Aber ansonsten nochmals nett aufbereitet.

    Müsste die Zeile:
    $logger = loggerFactory( $config[‚logger_type‘], $config );

    nicht

    $logger = loggerFactory( $config[‚logger_type‘], $config[‚filename‘]);

    heißen? Und in der Zeile
    $logger = new TextLogger( $params[‚filename‘ );

    fehlt eine „]“ (eckige Klammer zu). 😉

    Reply
  3. Factories sind durchaus schön haben aber den Nachteil dass der Programmcode eine Abhängigkeit zur Factory erhält und man somit innerhalb eines Testcases keine Möglichkeit hat den Rückgabewert der Factory zu beeinflussen.

    Was aber nicht heißt dass Factories generell böse sind, man muss nur schauen wie man das Pattern am sinnvollsten im eigenen Code einsetzt 🙂

    Reply
  4. Sehr schönes, aber auch irgendwie klassisches Beispiel für Factories. Setzt man hingegen bestimmte Klassen hauptsächlich zur Gewährleistung der Typsicherheit ein (Stichwort Type Hints), so kann man mittels einer Factory sehr schön aus sehr verschieden zusammengesetzten Arrays die passenden Objekte erzeugen. Array in die Factory hineinkippen, passendes Objekt kommt umgehend zurück. Man hat dann die Logik, wie aus unstrukturierten Daten „richtige“ Objekte werden, in einer einzigen Stelle zusammengefasst.

    Reply
  5. Genau. Sinnvolle Einsatzgebiete sind vor allem:

    – Caches
    – Logger
    – Datenbankenmanager
    – Sessionmanager
    – Configmanager

    Hat jemand noch weitere Vorschläge wo das Pattern Sinn macht? Oder wo es vielleicht gar nicht passt?

    Reply
  6. Sinn macht eine Factory eigentlich generell immer dann, wenn während der Laufzeit ein Verhalten festgelegt wird. Also immer dann, wenn ich noch gar nicht weiß, wie etwas ablaufen wird.

    Ich habe es vor Kurzem für mich neu entdeckt und mich auch neu begeistert bei einem Projekt, welches Daten an verschiedene Server von Unternehmen sendet. Die entsprechenden SOAP Schnittstellen waren alle Grundverschieden und nachdem der User einen Button geklickt hat, musste das Programm eben entscheiden, welche Schnittstelle genutzt werden soll. Über eine Factory war das sehr schön komfortabel zu lösen.

    Reply
  7. Die Beispiele sind ja alle recht low-level (Logger, DB, Session). Factories machen natürlich auch innerhalb der Business-Logik Sinn. Beispielsweise könnte ich mir vorstellen das man im Context eines Webshops Factories für Zahlweisen und Versandarten baut. Die entsprechenden Implementierungen wissen wie mit den Daten umzugehen sind (z.B. Kommunikation mit Payment-Dienstleister) und ev. auch welche Felder im Frontend angezeigt werden sollen (Bankdaten Kreditkartendaten).

    Reply
  8. Dem ist kaum was hinzuzufügen. Eventuell noch: natürlich bietet es sich an im Sinne der Typsicherheit darauf zu achten, dass die zurückgelieferten Objekte alle eine gemeinsame (abstrakte) Basisklasse haben und/oder ein gemeinsames Interface implementieren. Je nachdem was passender ist.

    Neben der oben beschriebenen „Factory Method“ sollte man aber vielleicht auch einen Blick auf das größere Pattern, die „Abstract Factory“ werfen. Diese bietet vmtl. auch mehr Stoff zur Diskussion.

    Siehe Wikipedia: http://de.wikipedia.org/wiki/Abstrakte_Fabrik

    Reply
  9. @Sven Input-/Output-Plugins
    Beispiele:
    – Es soll etwas von Format-A in Format-B umgewandelt werden.
    – Ein API soll Antworten in verschiedenen Formaten ausgeben.
    – Eine PHP-Applikation soll via HTTP, STDIN oder was auch immer angesprochen werden können und ihren Output als HTML, XML, reinen Text oder sonstiges auf der Konsole ausgeben, an einen Browser via HTTP zurückgeben oder eine Datei auf Filesystemebene erzeugen und ablegen.

    Reply
  10. Ich habe auch immer wieder davon gelesen und versucht es umzusetzen, nur konnte ich wirklich keinen Vorteil für mich finden.

    zB Logger Beispiel
    Was spricht hier gegen eine Funktion, die das Objekt zurück gibt?
    zB:

    function makeLogger($type, $args)
    {
    switch_case $type
    {
    case ‚txt‘:
    $logger = new TextLogger($args);
    brake;
    case ‚db‘:
    $logger = new DbLogger($args);
    brake;
    }
    return $logger;
    }

    $applicationLogger = makeLogger(‚db‘);
    Bei Klassen benötigt man „bisschen“ mehr Code und da ich ziemlich faul bin, versuch ich mir das zu sparen.

    Reply
  11. @ragtek Und wo willst du die Funktion unterbringen?
    Sowas gehört einfach in eine Klasse. Alleine schon wegen der Testbarkeit. Sonst müsstest du die Datei, die die (ober auch noch weitere solcher Funktionen) enthält on-demand immer wieder inkludieren. Macht man einfach nicht. 🙂

    Aber ehrlich gesagt verstehe ich dein Problem nicht. Wo ist der große Aufwand, wenn man statt der Funktion in einem losen PHP-Skript diese Funktion als Methode einer eigenständigen Klasse implementiert? Ist doch im Aufwand keinerlei Unterschied. Bringt nur Vorteile.

    Reply
  12. Um ehrlich zu sein, wundert es mich, dass im Zusammenhang mit dem Factory Pattern noch garnicht Dependency Injection angesprochen wurde.

    In Kombination kommt man damit sehr weit und schafft eine sehr gut wiederverwendbare API bei sauberer Anwendung.

    Ich hatte dazu vor kurzem eine Notiz an mich selbst geschrieben (Factory + Dependency Injection verstecken sich im Thema, sind aber letztendlich der Kern): http://julian.pustkuchen.com/registry-statt-singleton

    Reply
  13. Naja, Datenbank wurde ja schon öfters erwähnt, aber ich habe mal etwas gemacht (für Reporting/ETL), was eine Datenquelle mittels Factory erzeugt hatte.
    Dies konnte alles mögliche an 2-Dimensionalen Daten (Tabellenstruktur) sein:
    – SQL-Query
    – CSV
    – definiertes XML (auch Soap)
    – Log-Dateien
    – aufbereitete Objecte einer Masteraplication

    Ich mag dieses Muster einfach, nur die Schnittstellen müssen klar sein. 😉

    @ragtec: Manchmal braucht man einfach mehr wie eine Methode, denke dann wird es mit einfachen Funktionen unübersichtlich. Klar kann man pro includeter Datei dann mehrere Funktionen definieren. Aber schön ist was anderes. Jede Funktion braucht dann ein ‚Prefix‘ (zB.: $sFunctionName.’_execute()‘), damit unterschiedliche Dateien gleichzeitig genutzt werden können. Bei Klassen heißt nur die Klasse anders, die Methoden haben identische Namen.
    Und wenn die einzelnen ’nachgeladenen‘ Klassen sich von einer abstrakten Klasse ableiten, steht jeder Klasse weitere Funktionalität zu Verfügung (zB: Validierung), die man sonst immer irgendwie anders bekommen muss.
    Ein weiterer Vorteil ist, dass es sich um ein Pattern handelt und andere, die das kennen sich wesentlich schneller in den Code einarbeiten.

    Reply
  14. Also ich weis nicht, das Factory Pattern begeistert mich nicht wirklich.

    Mal ganz davon abgesehen, dass man sich dadurch eine Abhängigkeit schafft, die beim Testen durchaus Probleme Verursachen kann.

    Man macht sich damit komplett Abhängig von dieser einen Klasse, die man weder austauschen noch erweitern kann, ohne den Quellcode dieser Klasse bzw. des ganzen Projektes zu verändern.

    Ich bin der Meinung, dass selbst ein Logger per Dependency Injection eingefügt werden sollte.

    Und wenn man diesen Logger mittels einer Konfiguration auswählt, macht man halt $logger = new $Logger_from_config;

    Reply
  15. OK danke für die Antworten:)

    Mir fehlt hier dann vermutlich einfach die Routine, da ich (leider) nicht mit anderen zusammenarbeite und sich dadurch wohl auch niemand mit meinem Code auseinand setzen muss…..

    Gibt es sonst noch irgendwelche Beispiele ausser logger wo man sowas einsetzen kann?
    Im PHP Design Pattern Buch ist soweit ich mich erinnere auch nur das logger Bsp. drinn.

    Reply
  16. Und noch eine Frage: Wo würde man dann

    <?php

    public function loggerFactory( $loggerType, $params )
    {
    switch $loggerType
    {
    case 'text':
    // ein wenig defensives programmieren hier …
    $logger = new TextLogger( $params['filename' );
    break;
    case 'db':
    // ein wenig defensives programmieren hier …
    $logger = new DbLogger( $params['database_name' /* … */ );
    break;
    default:
    throw new Exception( 'Logger not found' );
    }
    return $logger;
    }
    platzieren?

    Reply
  17. @ragtek:
    Lies mal den Eintrag von Stephan Hochdoerfer durch.
    Das mit den Versand/Paymentmodulen in Shops ist wirklich gang und gebe.

    Reply

Leave a Comment.

Link erfolgreich vorgeschlagen.

Vielen Dank, dass du einen Link vorgeschlagen hast. Wir werden ihn sobald wie möglich prüfen. Schließen