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.