Explosion Driven Development
In Zeiten von strenger statischer Typisierung und guten Compilern ist es normal all seine Objekteigenschaften, Funktionen und Methoden mit Type Hints zu versehen und diese auch automatisch überprüfen zu lassen.
Auch die Argument Validation und Konvertierung ist zu einem festen Bestandteil vieler Frameworks geworden und aus moderner Anwendungsentwicklung kaum noch wegzudenken.
Das Ganze dient vor allem dazu Programmierfehlern und Eingabeproblemen vorzubeugen und damit im Endeffekt für den Benutzer sichtbare Fehler zu vermeiden und das Programm im allgemeinen stabiler und “qualitativer” zu programmieren.
Doch was ist mit nicht statisch typisierten oder interpretierten Programiersprachen wie PHP, JavaScript oder Ruby?
In den gängigen Frameworks wird auch hier schon auf Validation und Type Converter gesetzt und es gibt auch für die statische Typisierung verschiedene Lösungen. JavaScript kann man durch TypeScript erzeugen, für Ruby gibt es Sorbet und in PHP wird bereits seit der Version 5.0 die Liste der möglichen Typdeklarationen erweitert. Ab PHP 7.4 wird es auch typisierte Objekteigenschaften geben.
Dennoch gibt es immer wieder Fälle, in denen Variablen dynamisch oder gar nicht typisiert sind und eine Überprüfung im Code notwenig wird. Es stellt sich die Frage, wie dann damit umgegangen wird.
Angenommen wir haben folgende PHP Methode:
FilterArray::whereValueFirstCharacterIsNot(array $array, string $string): array
Wir können in der Signatur bereits festlegen, dass $array ein Array sein muss und $string ein String. Was wir nicht festlegen können ist, dass in dem Array ausschließlich Strings vorkommen. Sollte in dem Array ein Objekt sein können wir dieses nicht auf den ersten Buchstaben prüfen, da ein Objekt kein String ist.
In PHP werden viele Fehler (abhängig von Einstellungen) einfach unterdrückt, strikte statische Typisierung ist optional und manchmal wird auch ein stiller type cast angewendet. Das kann zu unvorhergesehenen und schwer zu findenden Problemen führen.
Ein Beispiel: array_unique() expects parameter 1 to be array, null given
Im Code wird für ein array_unique null statt einem Array übergeben. In einer Produktivumgebung würde der Fehler im schlimmsten Fall für den Entwickler maximal als Warning in das Log eingehen. Ohne weitere Informationen und gänzlich ohne Backtrace. Der Entwickler hat kaum eine Chance bei einer Utility-Methode herauszufinden, wo das Problem liegt, da diese für gewöhnlich an sehr vielen Stellen im Code verwendet werden.
Einfacher würde sich der Entwickler tun, wenn ein Backtrace zur Verfügung stünde, denn dort ist aufgezeichnet welcher Pfad durch den Code zu dem fehlerhaften Aufruf führt. Backtraces stehen dann zur Verfügung, wenn eine Exception bzw. ab PHP 7.0 ein Throwable geworfen wird. Diese können im Produktivsystem auch über externe Tools wie Sentry oder Rollbar verfolgt werden.
Darüber hinaus bietet sich die Möglichkeit an direkt im Code mittels try-catch-Block auf ein solches Problem zu reagieren.
Kehren wir zurück zu unserem Beispiel. Demzufolge muss im Code überprüft werden, dass es sich bei jedem einzelnen Element des Arrays um einen String handelt.
Nun gibt es einen klaren Fehler, wenn das Array einen Wert enthält, der kein String ist. Noch besser ließe es sich in diesem konkreten Fall lösen, indem wir PHP-interne Funktionen dafür verwenden.
Hier übernimmt der String Type Hint im Callback die Funktion der Typenprüfung. Sollte das Array nun einen nicht-String-Wert enthalten, wirft PHP einen TypeError. Ein TypeError ist ein Throwable und kann daher abgefangen werden.
Sollte man sich im Code nicht absolut sicher sein, dass das Array ausschließlich Strings enthält, umgibt man den Methodenaufruf mit einem try-catch-Block und kann so auf den Fehler reagieren.
Die Arbeit mit Exceptions ist nicht nur sinnvoll um Programmier-, Logik- und Benutzereingabefehler zu verarbeiten, sondern auch um an entsprechender Stelle darauf zu reagieren.
Stell dir vor, du sollst eine Schnittstelle anbinden, die Formulardaten verarbeitet. Da du bereits einige Erfahrung mit Schnittstellen hast, abstrahierst du die direkte Kommunikation mit dem Endpunkt in einem Treiber und versteckst den Treiber hinter einer abstrahierten Fassade (Facade Pattern). Damit entkoppelst du den Code von der Treiberimplementierung. Das Ganze geht produktiv. Der Kunde ist zufrieden, weil diese Formulardaten an seiner Schnittstelle ankommen und du bist glücklich, weil du einen guten Entwurf und eine saubere Implementierung gemacht hast.
Nun kommt der Tag, an dem die Schnittstelle offline ist (oder noch schlimmer: die API bricht). Dein Programm erzeugt nun ganz kuriose Fehler, da nicht damit gerechnet wurde, dass diese Schnittstelle einmal anders als erwartet reagieren könnte.
Wie hätte das verhindert werden können?
Durch Explosion Driven Development!
Bei der Abfrage der API sollte nicht nur die Konnektivität geprüft werden, sondern auch die Schema-Version - falls vorhanden - sowie die Antwort der Schnittstelle.
Ist der Endpunkt nicht verfügbar kann dies bereits im Treiber abgefragt werden. Dieser wirft eine ConnectionErrorException. Da der Endbenutzer mit dieser Exception für gewöhnlich nichts anfangen kann und einen schlechten Eindruck von der Anwendung/Webseite bekommt, sollte diese auf jeden Fall weiter behandelt werden.
Der Treiber hat jedoch keine Ahnung, ob er nun für die Abfrage der Formularfelder oder das Versenden der Daten eine Verbindung mit der Gegenseite herstellt. Dies ist auch nicht seine Aufgabe. Die Fassade koordiniert hierbei den Treiberaufruf. Es ist Aufgabe der Fassade die ConnectionErrorException weiter zu verarbeiten. Die aufgerufene Treibermethode wird mit einem try-catch-Block versehen und die Exception abgefangen. Der Misserfolg kann inklusive Backtrace zur Fehlerbehebung geloggt werden und die Weiterverarbeitung des Fehlers beginnt.
Die Fassade wird keine weitere Methode des Treibers aufrufen, da sie weiß, dass die Verbindung zur API gescheitert ist. Das ist ihre Aufgabe. Danach wirft sie eine EndpointNotAvailableException, die die ConnectionErrorException als previous exception hat.
Nun liegt es an dem Controller die EndpointNotAvailableException abzufangen und entsprechend darauf zu reagieren. Im Controller kann nun entschieden werden, ob dem Benutzer eine Nachricht angezeigt wird, dass das Formular nicht verfügbar ist oder eine Version aus dem Cache gerendert wird und die abgesendeten Daten zwischengespeichert werden bis die Schnittstelle wieder verfügbar ist.
Der Benutzer sieht nun entweder eine Aussagekräftige Fehlermeldung oder bemerkt im besten Fall nichts von dem Schnittstellenausfall.
Explosion Driven Development ermöglicht es, Fehler auf jeder Zuständigkeitsstufe entsprechend zu behandeln und sinnvoll zu reagieren. Dabei ist es wichtig die Zuständigkeit jeder Klasse zu respektieren.
Der Treiber kümmert sich um den Verbindungsaufbau und die schnittstellenspezifische Kommunikation (JSON, SOAP / REST, GrapQL, gRPC) und meldet Fehler, die dort Auftreten. Es ist nicht Aufgabe des Treibers aus einem Fehler eine sinnvolle Benutzerausgabe zu machen. Außerdem würde dem Treiber der passende Kontext fehlen.
Die Fassade kümmert sich im Programm um die Abstrahierung des Treibers. Den Controller interessieren jedoch keine Implementierungsdetails. Somit konvertiert die Fassade die treiberspezifische Exception in eine treiberagnostische Exception. Dies vervollständigt auch die Abstrahierung des Treibers auf Fehlerbehandlungsebene. Außerdem kümmert sich die Fassade als zentraler Kommunikationspunkt um das Logging.
Der Controller hat die Aufgabe Daten aus einer Quelle an das View zu übergeben. Wenn diese Daten nicht zur Verfügung stehen kann der Controller Alternativen ausmachen. Die Genannten sind
a) eine Version der Daten aus dem Cache, eventuell mit dem Hinweis versehen, dass diese nicht den aktuellsten Daten entsprechen könnten (z.B. bei Zugverbindungen) oder
b) eine aussagekräftige (oder beschwichtigende) Fehlermeldung (statt der Funktion) anzuzeigen.
Das sind natürlich nur zwei der möglichen Optionen.