thePHP.cc Logo English Kontakt
Wie heißen Deine Konstruktoren?

Wie heißen Deine Konstruktoren?

Trotz stark verbessertem Typsystem in den Versionen 7 und 8 unterstützt PHP kein Überladen von Konstruktoren. Muss ein Objekt auf unterschiedliche Weisen erzeugt werden, behilft man sich daher mit sogenannten Named Constructors. Gibt es Best Practices für deren Benennung?

Jeden letzten Freitag im Monat haben wir Office Hours, das ist eine offene Videokonferenz, in der wir dazu einladen, zwanglos mit uns über professionelle Software-Entwicklung mit PHP und verwandten Technologien zu sprechen. Es freut mich, gerade zu erleben, wie sich unsere Office Hours von Monat zu Monat mehr und mehr zu einem Treffpunkt der deutschen PHP-Community entwickeln.

Letzte Woche ergab sich während der Office Hours eine Diskussion zur Namensgebung von Named Constructors. Ich habe dabei einige Beispiele gegeben, wie ich Named Constructors in meinen Projekten benenne. Da ich am Wochenende Nachfragen dazu per E-Mail erhielt, habe ich hier meine Gedanken dazu aufgeschrieben.

from()-Methoden

Eine weit verbreitete Art der Namensgebung ist from() , beziehungsweise daraus abgeleitete Variationen. Da Named Constructors traditionell hauptsächlich für Wertobjekte zum Einsatz kommen, ist dieser Ansatz erst einmal eine gute Idee, denn der Methodenname macht deutlich, dass ein Objekt aus anderen Objekten bzw. aus skalaren Daten erzeugt wird.

Bis zur Version 7 war PHP eine wenig typsichere Sprache, mittlerweile kann man mit PHP auch sehr gut typsicher programmieren . Wo man also früher Methodennamen wie fromInt(...) oder fromString(...) brauchte, kann man heute eine skalare Typdeklaration verwenden und bei gleicher Lesbarkeit mehr Typsicherheit erzielen: aus fromString($value) wird dann from(string $value) .

Das wirft aber erneut das Problem auf, dass wir die Methode from() nicht überladen können. Was also tun, wenn wir in der Lage sein wollen, ein Objekt aus verschiedenen Typen zu erzeugen? Wenn wir ehrlich sind, dann gibt es in einer typsicheren Welt gar nicht mehr so viele gute Gründe dafür. Nehmen wir das folgende Beispiel:

class   SomeValueObject
{
     public   static   function   fromInt ( int   $value ) :   self
     {
         ...
     }
 
     public   static   function   fromString ( string   $value ) :   self
     {
         ...
     }
}

Wenn wir alternativ nur eine Fabrikmethode anbieten

class   SomeValueObject
{
     public   static   function   from ( string   $value ) :   self
     {
         ...
     }
}

dann kann man natürlich argumentieren, dass der Aufrufer nun für den Type Cast verantwortlich ist und die dafür gegebenenfalls nötige Fehlerbehandlung zu dupliziertem Code führt. Schließlich schreiben wir

$object   =   SomeValueObject :: from ( (string)   $integer ) ;

überall da, wo das Wertobjekt erzeugt werden muss. Allerdings: in einer zunehmend typsicheren Welt ist ein int nun mal bereits ein int , denn wir können unserem Parameter-Objekt (häufig ist das zugleich das Request-Objekt) Methoden geben, die uns bestimmte Typen zurückliefern:

final   class   ProcessFormRequest
{
     ...
 
     public   function   parameterAsString ( string   $name ) :   string
     {
         return   (string)   $this -> parameter ( $name ) ;
     }
 
     public   function   parameterAsInt ( string   $name ) :   int
     {
         return   (int)   $this -> parameter ( $name ) ;
     }
 
     ...
}

Versteht man eine Variable als dynamischen Typ, wie es in der klassischen PHP-Entwicklung der Fall war, dann tritt das Problem der Typkonvertierung erst relativ spät auf, nämlich wie hier im Beispiel bei der Erzeugung des Wertobjekts. Das ist so, weil wir einen Wert mit nicht genau bestimmten Typ relativ weit in die Anwendung hineingereicht haben.

Mehr Typsicherheit

Wenn wir stattdessen eher an typisierte Variablen denken, dann sollten wir den Typ eines Parameters möglichst früh festlegen. Dabei hilft uns das oben gezeigte ProcessFormRequest , das wir beispielsweise als Wrapper um (oder Adapter für) das Request-Objekt des verwendeten Frameworks gestalten können:

final   class   ProcessFormRequest
{
     private   HttpRequestOfYourFramework   $httpRequest ;
 
     ...
 
     public   function   parameterAsString ( string   $name ) :   string
     {
         return   (string)   $this -> httpRequest -> get ( $name ) ;
     }
 
     ...
}

Das Problem, tief innerhalb der Anwendung ein Wertobjekt aus verschiedenen skalaren Typen erzeugen zu müssen, stellt sich dann nicht mehr, weil die Typkonvertierung deutlich näher an die "vordere Front" des Programms gerückt ist.

In diesem Sinne sind Methodennamen wie create() oder make() , wie sie in gängigen Frameworks und Projekten häufig verwendet werden, vermutlich genauso gut wie from() . Dies trifft aber hauptsächlich auf klassische Wertobjekte zu. Services und Infrastruktur-Objekte diskutieren wir weiter unten.

Noch kleinere Objekte

Ein weiterer häufig zitierter Use Case für Named Constructors ist die Erzeugung eines HTTP-Requests auf zwei Arten, einmal aus Parametern und einmal aus den superglobalen Variablen. Mehr oder weniger jedes Framework macht das so, um Entkopplung der Anwendung vom globalen Systemzustand in Form der superglobalen Variablen zu erzielen:

class   HttpRequest
{
     public   static   function   fromSuperglobals ( ) :   self
     {
         ...
     }
 
     public   static   function   from ( ... ) :   self
     {
         ...
     }
}

Ich war lange glücklich und zufrieden mit einer solchen Lösung, bis ich realisiert habe, dass mein Request-Objekt noch immer zu groß ist, weil es nämlich für zwei unterschiedliche Dinge verantwortlich ist, und zwar für das Testen und für den Produktivbetrieb.

Man kann darüber streiten, ob es eine gute Idee ist, Code speziell für Tests zu schreiben. Ich würde allerdings argumentieren, dass es eine wesentliche Aufgabe des Frameworks ist, das Testen der Anwendung zu fördern und zu erleichtern. Ob das zu einer so engen Verzahnung von Testcode und Produktivcode führen sollte wie das beispielsweise in Laravel der Fall ist, ist eine andere Frage, auf die ich hier und heute nicht weiter eingehen will.

Wenn wir - zurück in unserem Beispiel - den echten und den "falschen" HTTP-Request in zwei Objekte aufteilen, dann haben wir zwei kleinere und spezifischere Objekte, die jeweils nur einem Zweck dienen. Das ist ein besseres Design.

In meinem HTTP-Framework für die Auslieferung von Content gibt es dafür beispielsweise zwei getrennte Klassen, die ein gemeinsames Interface implementieren, und zwar

final   class   RealHttpRequest   implements   HttpRequest
{
     public   static   function   fromSuperglobals ( ) :   self
     {
         ...    
     }
 
     ...
}

und

final   class   FakeHttpRequest   implements   HttpRequest
{
     private   string   $method ;
     private   string   $path ;
     private   array   $formData ;
 
     public   static   function   get ( string   $path ) :   self
     {
         return   new   self ( 'GET' ,   $path ) ;
     }
 
     public   static   function   head ( string   $path ) :   self
     {
         return   new   self ( 'HEAD' ,   $path ) ;
     }
 
     public   static   function   post (
         string   $path ,
         array   $formData
     ) :   self
     {
         return   new   self ( 'POST' ,   $path ,   $formData ) ;
     }
 
     ...
}

Der "gefälschte" HTTP-Request bietet mehrere explizite Fabrikmethoden an, um nach Bedarf den passenden Request zu erzeugen, beispielsweise:

$request   =   FakeHttpRequest :: get ( '/the-url-path' ) ;

In der Bootstrap-Datei der Anwendung steht dagegen:

$request   =   RealHttpRequest :: fromSuperglobals ( ) ;

Wir profitieren definitiv von besserer Lesbarkeit, wenn wir mehrere Fabrikmethoden anbieten. Hierbei geht es aber nicht mehr um die Frage, aus welchen Daten wir den Request erzeugen, sondern welche Art von Request wir erzeugen wollen.

Verschiedene Arten der Erzeugung

Auch wenn wir bereits akzeptiert haben, dass die Erzeugung eines Wertobjektes aus verschiedenen skalaren Daten an sich kein häufiger Use Case mehr ist, gibt es dennoch häufig die Anforderung, ein Objekt auf unterschiedliche Arten zu erzeugen. Immer dann, wenn Serialisierung oder Persistenz im Spiel ist, müssen wir in der Lage sein, ein Objekt in seinem fachlichen Lebenszyklus nach der initialen Erzeugung wiederholt aus technischen Gründen zu erzeugen, nämlich wenn es aus der Persistenz geladen wird.

Nehmen wir eine UUID als Beispiel. Die fachliche Erzeugung einer UUID entspricht grob gesprochen dem Auswürfeln einer Zufallszahl. Wir benennen den Konstruktor entsprechend:

final   class   UUID
{
     ...
 
     public   static   function   generate ( ) :   self
     {
         ...
     }
 
     ...
}

Wollen wir ein Objekt, welches diese UUID repräsentiert, später erneut erzeugen, verwenden wir:

final   class   UUID
{
     ...
 
     public   static   function   from ( string   $uuid ) :   self
     {
         ...
     }
 
     ...
}

Ein anderes Beispiel wäre ein Objekt, welches ein Ereignis (Event) repräsentiert. Initial wird das Event aus den vorhandenen Daten erzeugt:

class   EmailVerificationRequestedEvent   implements   Event
{
     ...
 
     public   static   function   from (
         AccountId   $accountId ,
         EmailAddress   $emailAddress
     ) :   self
     {
         ...
     }
 
     ...
}

Wird das Ereignis später aus einem Event Store geladen, wo es als JSON serialisiert abgelegt war, verwenden wir einen weiteren Konstruktor:

class   EmailVerificationRequestedEvent   implements   Event
{
     ...
 
     public   static   function   fromJson ( Json   $json ) :   self
     {
         ...
     }
 
     ...
}

Die Klasse Json kapselt dabei generisch ein Json-Dokument und ist sowohl für das json_decode() als auch für eine saubere Fehlerbehandlung beim Zugriff auf Daten aus dem Dokument zuständig.

Man könnte in diesem Beispiel freilich auch einen separaten Data Mapper implementieren, der das Event-Objekt aus den JSON-Daten erzeugt. Ich finde es aber besser, bei Veränderungen nicht Mapper und das Event-Objekt selbst parallel editieren zu müssen, sondern den Mapping-Code direkt in der fromJson() -Methode und damit im Objekt selbst zu haben. (Bei Entitäten oder gar Aggregates wäre meine Meinung komplett anders!)

Infrastruktur

Ich bin mittlerweile in einigen meiner Projekte dazu übergegangen, durchgängig für alle Objekte einen Named Constructor zu schreiben, auch wenn das technisch streng genommen überflüssig sein mag. Um ein Objekt zu erzeugen, das mittels Dependency Injection über den Konstruktor mit Kollaboratoren versorgt wird, verwende ich den Methodennamen collaboratingWith() :

$dispatcher   =   SynchronousEventDispatcher :: collaboratingWith (
     $eventHandlerLocator
) ;

Ein Event Dispatcher wird nicht aus einem Locator erzeugt, sondern arbeitet mit diesem zusammen. Das from() wäre hier definitiv keine passende Bezeichnung mehr, auch make() oder create() wären meiner Ansicht nach wenig treffend.

Als weiteres Beispiel haben wir hier die Fabrik eines Frameworks, das mit der Fabrik der Applikation zusammenarbeiten muss, da nur diese die vom Anwendungsentwickler geschriebenen Objekte erzeugen kann:

final   class   PeregrineFactory   implements   Factory
{
     ...
 
     public   static   function   collaboratingWith (
         ApplicationFactory   $applicationFactory
     ) :   self
     {
         return   new   self ( $applicationFactory ) ;
     }
 
     ...
}

Ich mag es, dass durch die Namensgebung eine Zusammenarbeit der Objekte sozusagen "auf Augenhöhe" suggeriert wird. Ich habe auch das Gefühl, dass mir die namentliche Abgrenzung zwischen from() und collaboratingWith() dabei hilft, die Verantwortlichkeiten zwischen den einzelnen Objekten besser zu verteilen, beziehungsweise die Objekte noch prägnanter auszugestalten.

Ausnahmsweise

Ich verwende Named Constructors beziehungsweise Fabrikmethoden auch konsequent für Ausnahmen (Exceptions). Man muss sich dabei stets daran erinnern, dass PHP nicht die Zeile in den Stacktrace schreibt, in dem das throw steht, sondern die Nummer der Zeile, in der das new steht. Dass es hier einen Unterschied gibt, fällt natürlich erst dann auf, wenn man Named Constructors verwendet, denn ansonsten stehen das new und das throw normalerweise in der gleichen Zeile. Wenn man das aber weiß und dann im Zweifel in den nächsten Frame des Stacktrace schaut, um herauszufinden, wo eine Exception geworfen wurde, dann ist das kein Problem.

Um eine Exception zu erzeugen, können wir also ebenfalls eine statische Fabrikmethode beziehungsweise einen Named Constructor benutzen:

throw   LongbowException :: noCommandSpecifiedFor ( $commandHandler ) ;

Ich finde, das macht den Code kompakt und sehr gut lesbar, außerdem findet man konkrete Aufrufstellen sogar noch etwas einfacher und. Vor allem aber habe ich alle Exception-Messages an einem Ort, nämlich in der Exception-Klasse selbst, konzentriert und kann dort viel leichter für Konsistenz und Einheitlichkeit sorgen, als wenn die Texte quer durch die Codebasis verstreut sind.

Exceptions sind übrigens wieder ein sehr gutes Beispiel für Objekte, die aus verschiedenen Daten erzeugt werden:

class   LongbowException   extends   RuntimeException
{
     public   static   function   noCommandSpecifiedFor (
         string   $commandHandler
     ) :   self
     {
         return   new   self ( sprintf ( '...' ,   $commandHandler ) ) ;
     }
 
     public   static   function   factoryHasNoMethod (
         object   $factory ,  
         string   $shortName
     ) :   self
     {
         ...
     }
 
     public   static   function   noEventSpecifiedFor (
         string   $eventHandler
     ) :   self
     {
         ...
     }
}

Allerdings würde hier niemand auf die Idee kommen, die Methoden from() zu nennen, oder?

Parameterlose Konstruktoren

Es verbleiben ein paar wenige unschöne Edge Cases für "gute" Namen, beispielsweise Konstruktoren, die keine Parameter haben:

$factory   =   Factory :: from ( ) ;

Das sieht irgendwie doof aus, oder? Ich habe das so gelöst:

class   Factory   implements   ApplicationFactory
{
     ...
 
     public   static   function   withDefaults ( ) :   self
     {
         return   new   self (
             Configuration :: from (
                 Environment :: fromSuperglobals ( )
             )
         ) ;
     }
 
     ...
}

Hier betont der Name, dass die Fabrik mit Default-Werten initialisiert wird. Natürlich kann ich das auch anders machen, wenn es der Use Case erfordert:

class   Factory   implements   ApplicationFactory
{
     ...
 
     public   static   function   with (
         Configuration   $configuration
     ) :   self
     {
         ...
     }
 
     ...
}

Insbesondere bei Top-Level-Aufrufen, beispielsweise dort, wo man in der Bootstrap-Datei die Anwendung initialisiert, finde ich parameterlose Konstruktoren klasse. Für Objekte mitten in der Anwendung sehe ich das meist anders.

Konstruktoren und Fabrikmethoden

Um die Frage "Wie heißt dein Konstruktor?" zu beantworten, muss man sich fragen, welchem Zweck der Konstruktor dient. Es ist gerade in PHP nicht so, dass der Konstruktor das Objekt erzeugt, sondern er ist lediglich eine Interzeptormethode, die automatisch aufgerufen wird, nachdem das Objekt bereits erzeugt wurde (sonst würde $this im Konstruktor ja nicht funktionieren). Ein Konstruktor initialisiert, konfiguriert oder parametrisiert also ein Objekt, je nachdem, wie man das nennen möchte.

Eine statische Fabrikmethode sieht nach außen genauso aus wie ein Named Constructor, dient aber dem Zweck, eine Implementierung auszuwählen, ohne dass der Client deren konkreten Klassennamen kennen muss. Der Übergang zwischen beiden Konzepten ist fließend und es gibt einige Überlappung. Wenn ich Language::en() aufrufe, dann ist weniger wichtig, ob ich eine Language -Instanz zurückbekomme oder eine Instanz von English , welches eine Subklasse von Language ist. Ist Language::en() nun ein Named Constructor oder eine Fabrikmethode?

In der Tat ist das eigentlich egal, uns insbesondere ist es egal, ob wir uns in Zukunft vom einen zum anderen weiterentwickeln. Genau das ist ein großer Vorteil des Ganzen. Wenn wir die new -Statements "wegsperren", indem wir sie in statische Methoden verbannen, dann haben wir unseren Code ein Stück weiter von Implementierungsdetails (hier: welche Klassen gibt es bzw. wie heißen diese) entkoppelt. Das ist vorwärtskompatibel, weil es uns ermöglicht, die Struktur unseres Codes zukünftig zu verändern, indem wir Subklassen einführen oder entfernen, und das ohne dass wir den aufrufenden Code anpassen müssen.

Alles in allem ist es vielleicht gar nicht so eine gute Idee, eine allgemeingültige "one size fits all"-Regel für die Benennung von Named Constructors zu erzwingen, zumal wir sie im Alltag in der Betrachtung nicht wirklich von Fabrikmethoden trennen. Viel sinnvoller scheint es, die Methodennamen differenzierter und fachlich aussagekräftig zu wählen. In diesem Zusammenhang sollten wir uns immer daran erinnern, dass bereits die Bezeichnung "Konstruktor" irreführend ist, weil hier nichts erzeugt, sondern lediglich ein Objekt initialisiert wird.

Nachtrag

Ich wurde gerade darauf aufmerksam gemacht, dass Andreas Möller , einer der Besucher unserer Office Hours, die Diskussion schon am Freitag zum Anlass genommen hatte, einen Blogpost zu schreiben. Vielen Dank für Deine Gedanken zum Thema, Andreas.