Facebook
Twitter
Google+
Kommentare
32

Defensives Programmieren mit PHP

Heute geht es um die Technik des defensiven Programmierens. Ihr kennt es ja bestimmt vom Autofahren. Immer defensiv. Lernt man ja schon in der Fahrschule. Aber was bedeutet das? Ich würde es mal mit „immer mit den Fehlern der anderen rechnen“ übersetzen.

Übertragen auf die Softwareentwicklung heißt es genau das Gleiche. Geht einfach davon aus, dass die Verwender eurer Funktionen ein wenig auf den Kopf gefallen sind und gerne mal was falsch machen. Bereitet euch also drauf vor. Vielleicht nicht gleich auf den DAU, aber in die Richtung kann es gehen.

function fahrenheitToCelcius( $fahrenheit ) { return ( $fahrenheit - 32 ) * 5 / 9; }

An dem Beispiel sehen wir schon, dass schwache Typisierung das Defensive schon ein wenig erschwert. Was passiert in unserem Fall, wenn der Anwender einen String übergibt? Sollte das ganze Programm dann einfach abstürzen? Nö, eigentlich nicht. Eine Exception wäre schon ganz nett.

function fahrenheitToCelcius( $fahrenheit ) { if ( !is_number( $fahrenheit ) { throw new InvalidArgumentException( '$fahrenheit must be a number.' ); } return ( $fahrenheit - 32 ) * 5 / 9; }

So, schon kann man nichts mehr verbocken, wenn die Funktion durchgelaufen ist, dann funktioniert sie auch. Zumindest syntaktisch. Prüfen müsste ich jetzt noch, ob $fahrenheit größer ist als -459,67 (absoluter Nullpunkt).

function fahrenheitToCelcius( $fahrenheit ) { if ( !is_numeric( $fahrenheit ) { throw new InvalidArgumentException( '$fahrenheit must be a number.' ); } if ( $fahrenheit < -459.67 ) { throw new InvalidArgumentException( '$fahrenheit must be greater than -459.67.' ); } return ( $fahrenheit - 32 ) * 5 / 9; }

So,  ich glaube jetzt haben wir wirklich alle Gegebenheiten analysiert. Damit sollte die Funktion jetzt wirklich DAU sicher sein. Ihr merkt natürlich, dass wir auf einmal das siebenfache an Zeilen Code haben. Kann also relativ aufwendig sein.

Wichtig hierbei ist es das Gleichgewicht zwischen Nutzen und Kosten zu bestimmen. Nutzen ist in diesem Fall die Stabilität der Methode. Kosten ist der Mehraufwand für die sechs extra Zeilen Code.  Aber wem sag ich das. Irgendwann bekommt man ein Gespür dafür, wie DAU-sicher man arbeiten muss.

Wer sich noch ein wenig in das Thema einarbeiten will, der sollte sich mal assert anschauen. Das wäre hier das Mittel der Wahl, aber ich wollte es mir für einen anderen Artikel aufheben.

Ü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

32 Comments

  1. Ob assert() hier das geeignete Mittel der Wahl ist? Zumindest das Manual äussert sich da recht eindeutig:
    „Assertions should be used as a debugging feature only. You may use them for sanity-checks that test for conditions that should always be TRUE and that indicate some programming errors if not or to check for the presence of certain features like extension functions or certain system limits and features.

    Assertions should not be used for normal runtime operations like input parameter checks. As a rule of thumb your code should always be able to work correctly if assertion checking is not activated.“

    http://www.php.net/assert

    Reply
  2. Wenn man dieses Prinzip konsequent fortsetzt, hat man irgendwann bei einer Methode, die mehr Parameter besitzt, sehr viele Fehlermöglichkeiten, die man abfangen müsste. Solche Fehlerquellen bei jeder Methode, die man schreibt, durchzuführen, ist nicht nur lästig sondern auch zeitintensiv. Ich würde deshalb dafür sorgen, dass der ankommende Inhalt einer (selbstgewählten) Norm entspricht. So was ließe sich z.B. über invariante Datentypen realisieren, worüber ich vor einiger Zeit einen heise Developer-Artikel [1] gelesen habe. Durch konsequentes Einhalten der Invariante kann man somit sicherstellen, dass der Inhalt der o.g. Norm entspricht, womit man getrost auf die zusätzlichen Überprüfungen verzichten und sich auf das Wesentliche konzentrieren kann.

    In gewisser Weise ist es beleidigend, dass du einer ganzen Zunft unterstellst, auf den Kopf gefallen zu sein, denn unter dieser Prämisse wäre es nicht mehr sinnvoll, eigenen Code zu veröffentlichen. 😉 Will man damit nur einem Bruchteil der Entwickler helfen, stellt sich tatsächlich die Frage, ob es sich überhaupt rentiert, solche Überprüfungen durchzuführen, denn bei ausreichend guter Dokumentation sollte der Entwickler wissen, welche Garantien ihm von einer Methode geboten werden. Dass jeder Entwickler Fehler macht ist klar, aber ich finde, dass man in den Entwickler mehr Vertrauen setzen sollte – immerhin ist man als Entwickler kein Babysitter für Verwender des eigenen Codes.

    Insgesamt sollte man schon defensiv programmieren, aber nicht mit der Holzhammermethode, sondern mit eleganteren Lösungen wie den o.g. Invarianten.

    [1] http://www.heise.de/developer/Einhaltung-von-Invarianten-mit-dem-Value-Object-Pattern–/artikel/139597

    Reply
  3. Ich denke schon, dass hier assert() sogar eher bevorzugt werden soll. Mit dem eigenen Test und der Exception läuft man schnell in „Double-Validation“, was bedeutet, dass der Nutzer der Funktion natürlich gerne die Exception vermeiden möchte und deshalb selber testet, ob es sich um eine Zahl handelt. Damit macht er das gleiche, wie man selbst _innerhalb_ der Funktion. Lass das bei jeder Funktion auftauchen und schon hat man einiges an unnützem Overhead.

    Natürlich sind Assertions nur für die Entwicklung interessant, aber genau deshalb kann man sie über eine ini-Einstellung pauschal deaktivieren. Ich denke bei Funktionen/Methoden, die nicht direkt mit Endnutzern (-> Formular) in Kontakt kommen, sind sie das Mittel der Wahl. Solange ich eine Funktion schreibe, von der ich ausgehen kann, dass sie jemand benutzt, der auch lesen kann (-> DocBlocks), kann ich ebenso davon ausgehen, dass er sie richtig verwendet.

    Wie man oben sieht: Aus einer 1-Zeilen Funktion wurden mal eben 6 Zeilen. Darunter leidet auch die Lesbarkeit.

    Reply
  4. @andre: Ich habe nirgends die ganze Zunft angesprochen. Ich habe nur gesagt, dass man davon ausgehen soll, dass mal ein DAU die Methode benutzt.
    Ich kann für mich nur sagen, dass ich mich immer freue, wenn ich irgendwo was falsch mache, eine aussagekräftige Exception bekomme. Dass ich es nicht immer selbst mache, habe ich ja schon gesagt. Wichtig finde ich es trotzdem.

    Reply
  5. Ich bezweifel, dass man wirklich „davon ausgehen sollte“. Um dein Auto-Beispiel fortzusetzen: Du kannst auch nicht den Verkäufer verklagen, wenn du Gas und Bremse verwechselst.
    Deine „aussagekräftige Exceptions“ bekommst du — zumindest in der Entwicklung — auch über Assertions (dann vllt nicht unbedingt als Exception ;)). Das is dann vergleichbar mit dem Fahrlehrer, der dir sagt, was du falsch machst 😀

    Reply
  6. Genau, um das von KingCrunch bereits angesprochene Problem der „Doppelvalidierung“ zu vermeiden, würde ich den Ansatz fahren, aus dem Input das „maximal mögliche“ herauszuholen. Somit würde sich ein Cast zu float anbieten, womit die Methode fehlerfrei zuende geführt werden kann.

    lg

    Reply
  7. Juhu. Endlich mal wieder eine Diskussion 🙂 Ob die Methode wirklich viel unleslicher ist, weiß ich nicht. Man kann das ja alles Kapseln und auslagern, dann dampf man den Overhead ja auch ein. Außerdem bin ich ja Anwender der Funktion, für mich ist sie also eine Blackbox. Da ist mir das Verhalten nach Außen wichtiger, als das nach Innen.

    Reply
  8. Klar is das eine Blackbox, bloss man ruft ja keine Methode nach Verdacht, sondern nach Beschreibung auf. Wenn dort als Datatype „number“ aufgeführt ist, sollte ich mich nicht wundern, wenn sie nicht funktioniert, wenn ich stattdessen ein Objekt übergebe.

    Zur Leserlichkeit:
    Man sollte hier bedenken, dass grad mal ein einziger Parameter validiert wird. Und nehmen wir an, dass auch Prüfungen existieren, die nicht zwangsläufig in eine Exception enden, sondern (as Convenience) in einen Cast. Dann hat man womöglich sogar noch verschachtelte IFs für etwas, was eigentlich im DocBlock bereits erläutert wird.

    Bei assert() hat man anfangs einige Zeilen, die eben mit assert() anfangen. Hat man sowas schon mal gesehen, weiß man, dass man diese Zeilen gedanklich gefahrlos überspringen kann, weil sie nur auf die Vorbedingungen prüfen.

    Reply
  9. Ich denke, schlussendlich wird’s drauf ankommen, für welchen Zweck der Code geschrieben wird. Ich habe mich schon selbst oft genug in der besagten Situation befunden, den Code „vorzuvalidieren“, um die Exception einer verwendeten Bibliothek zu vermeiden. Alternativ könnte man sich noch eine Art „makeTheMostOutOfIt“-Parameter vorstellen, welcher – falls gesetzt – das von mir beschriebene Verhalten anwendet – ergo aus der Benutzer-Eingabe „63607 Wächtersbach“ ins Formfeld „Hausnummer“ per (int)-Cast die korrekte PLZ generiert. (Anmerkung am Rand: Du meintest bestimmt „is_numeric“, is_number ist zumindest keine PHP-Funktion).

    lg

    Reply
  10. @KingCrunch: Die assert Geschichte wollte ich ja auch verwenden, aber da werde ich mir erstmal die Doku nochmal zu durchlesen (siehe erster Kommentar), bevor ich mich hier zu weit aus dem Fenster lehne.

    @David: Auch in einem solchen Fall würde ich den User erstmal entscheiden lassen, bevor ich drauf los rechne. „Meinten sie …“
    (is_numeric … klar, hab`s geändert, danke)

    Reply
  11. @Nils: Das war überspitzt ausgedrückt (deshalb auch der Smiley).
    @David: So was sollte man besonders bei Benutzereingaben nicht machen. Zwar ist ein Int-Cast ungefährlich, aber wenn man versucht, etwas zu reparieren, dann läuft man Gefahr dabei etwas falsch zu machen und somit eine Sicherheitslücke entstehen zu lassen. Besser wäre es, dem Benutzer einen Hinweis zu geben, wie das Format auszusehen hat.
    @KingCrunch: Volle Zustimmung.

    Insgesamt finde ich es nicht lohnenswert alle möglichen Fehlerquellen zu suchen und sie zu eliminieren. Es sollte im Interesse des Verwenders liegen, keine Fehler zu machen und dabei kann man ihm nur helfen, indem man ihm genau sagt, worauf er sich verlassen kann. Dies schafft man, indem man den Quellcode präzise dokumentiert.

    Reply
  12. Ich finde den Ansatz an sich nicht dumm und auch nachvollziehbar. Doch gerade in PHP, dynamisch typisiert, für einige Sauereien zu haben, ist das unpassend. Da würde ich doch eher den Weg von KingCrunch gehen und mit assert arbeiten. Etwas anderes ist es vielleicht zum Beispiel bei Dateinamen. Eine FileNotFoundException ist durchaus angebracht. Aber zu mosern, weil der Übergabeparameter nicht nach einem gültigen Dateinamen aussieht (Dateiname „5“ als Integer zum Beispiel?) halte ich für PHP unpassend.

    Reply
  13. Sehr guter Ansatz, den ich so ähnlich auch verfolge. Was man da an Zeit reinsteckt, um ein paar Zeilen mehr zu schreiben, holt man nachher vielfach wieder rein, wenn man Fehler schnell findet und sich gar nicht erst durch die Dokumentation kämpfen muss, um nach der erwarteten Konvention zu gucken. Ausserdem hasse ich es, am Debugger zu hängen.
    Gute Fehlerbehandlung macht für mich gute Programme aus! Besonders gilt das für grundlegende Funktionalität, Klassen, die häufig und in vielen Projekten verwendet werden.

    Ausserdem, zum Thema „schaut halt in die Dokumentation“: wie hilft einem das, wenn ich der Methode eine Variable übergebe, die auf Grund eines vorangegangenen Bugs einen ungültigen Wert hat? In solchen Fällen kommt es zu ekligen Fehlerketten, der Fehler wird immer weiter durchgereicht und verschlimmert sich u. U. noch. Und dort, wo er dann schließlich augenfällig wird, ist man sehr weit von der eigentlichen Ursache entfernt. Dann kann man mitunter sehr lange suchen, weil man, vom Fehler ausgehend, zurücksuchen und die Ursache finden muss. Besonders fies sind dabei Fehler, bei denen z. B. lange Zeit fehlerhafte Daten gespeichert werden – bis einem das auffällt kann viel Schaden angerichtet sein.

    Im vorliegenden Beispiel würde ich wohl ohnehin eine Klasse „Temperature“ schreiben, die intern in Kelvin speichert. Wenn ich diese Klasse ordentlich implementiere, kann ich mich an anderer Stelle darauf verlassen und muss nicht die immer gleichen Prüfungen erneut schreiben.

    Reply
  14. @Claus-Christoph: In deinem letzten Absatz schreibst du über invariante Value-Objekte (siehe Kommentar 3), auch wenn du es nicht explizit erwähnst.
    Wenn man Eigenschaften präzise ausdrückt, lässt sich sogar beweisen bzw. widerlegen, ob diese Eigenschaft invariant ist. Ist es tatsächlich eine Invariante, kann man sich „an anderer Stelle darauf verlassen“, d.h. es wird eine Garantie für die Richtigkeit gegeben. Durch konsequentes Befolgen des Prinzips „Wenn du mir garantierst, dass die Eingabe richtig ist, garantiere ich, dass die Ausgabe richtig ist“ lassen sich die meisten Programmierfehler vermeiden, womit solche Überprüfungen nahezu obsolet sind. Natürlich müssen die Garantien dementsprechend sinnvoll sein.
    Das wichtigste an neuem Code ist dann, dass die Garantien erfüllt sind, wenn eine Methode aufgerufen wird. Willst du in deinem Beispiel das Temperature-Objekt an eine Funktion zum Umrechnen übergeben, so bekommst du die Garantie „Gib mir eine gültige Temperatur“ geschenkt, wenn die Richtigkeit eines Temperature-Objekts bereits garantiert ist.

    Reply
  15. @Andre Moelle: davon bin ich auch ausgegangen, der Heise-Artikel verwendet aber ein so unnötig komplexes Beispiel, dass ich den nicht durchkauen wollte, um das zu verifizieren.

    Überdies halte ich derlei großspurige Worthülsen für überflüssig. Domain Driven Design, Value Object Pattern…meiner Ansicht gehört sowas zu den OOP-Grundlagen.

    Reply
  16. Ich verstehe deinen letzten Absatz nicht. Habe ich tatsächlich Worthülsen verwendet? Wie dem auch sei: Egal ob Grundlagen oder nicht, wenn es ein Wort für etwas gibt, dann sollte es auch verwendet werden – das ist der Vorteil eines Vokabulars. Ook. Ook. 🙂

    Reply
  17. @Nils (+ Christoph)

    Warum muss eine Methode fahrenheitToCelsius prüfen, ob der Parameter ein korrekter Fahrenheit-Wert ist?
    Dann müsste die Funktion somethingToCelsius heißen, oder?

    Genau da setzt das von Andre angesprochene Value Object Pattern an. Damit erzeugt man z.B. ein VO Fahrenheit, dass auf jeden Fall korrekt ist (sonst wird es nicht erzeugt) und allen die damit arbeiten wird ein korrekter Typ garantiert, z.B.:

    $f = new Fahrenheit($value);
    $f->toCelsius();

    oder

    class…
    function toCelsius(Fahrenheit $f);

    Auf den ersten Blick kein großer Gewinn, allerdings sehr wohl von der expliziten Bedeutung und Benutzbarkeit. Abgesehen von der dadurch erreichten Typisierung und den Vorteilen daraus.

    Und Exceptions sind, tja, eben Ausnahmen. Aber eine Zuweisung von falschen Werten ist keine Ausnahme, sondern oft die Regel.
    😉 … aber ist eine andere Diskussion und nicht so wichtig.
    Es gibt meist viele Lösungen für ein Problem, gelle?

    @Christoph
    Sicherlich herrscht eine inflationäre Enwticklung in der Nennung vieler dieser sog. „Buzzwords“. Allerdings muss man auch sagen, dass die (möglichst) korrekte Benennung von Dingen lange Diskussionen und Irrtümer vermeidet. Vor allem DDD basiert sogar zwingend auf einem allgemein verständlichem Vokabular, um eben Übersetzungs- und Verständnisfehler zu vermeiden.

    Und zu den OOP-Grundlagen können nicht alle Konzepte (DDD), Werkzeuge (VO Pattern) gehören, denn dann wären es keine Grundlagen mehr.

    Bei DDD ist es so, dass es eine Sammlung von Konzepten und möglichen Ansätzen zur Lösung von komplexen Geschäftsanwendungen ist. Und es derzeit vermehrt Einzug in die PHP-welt hält. Allerdings fast immer aus struktureller Sicht umgesetzt und genau das ist es überhaupt nicht, worauf es bei DDD ankommt.

    Und bei den meisten Beispielen dazu und Implementierungen sind es eben keine komplexen, sondern fast ausschließlich CRUD-Anwendungen. Dafür ist DDD nicht „gemacht“.
    Wobei man viele einzelne Konzepte in jeder guten OOP-Anwendung durchaus einsetzen sollte, z.B. das erwähnte VO Pattern (welches allerdings leider allzuoft auch missverstanden wird).

    My $0.02
    Cheers!

    Reply
  18. @Don:
    Schaue ich mir die Java-Bibliothek an, dann finde ich da ein Value Object nach dem anderen. Und da die Java-Bibliothek nicht erst seit 2004 existiert, gehe ich mal davon aus, dass die das schon vor „Domain-Driven Design – Tackling Complexity in the Heart of Software“ so gekannt haben.

    Und natürlich sind VOs eine Grundlage von OOP – ich mag mich sogar dazu versteigen und sagen, dass sie DIE Grundlage sind. Denn zentrales Merkmal von OOP ist eben, dass man mit Objekten arbeitet, die ihre internen Werte kapseln und die Veränderung einer Objektinstanz nur über Methoden zulassen.

    (btw. ist mein Rufname „Claus“)

    Reply
  19. Sorry, noch eine Ergänzung:
    Es erscheint mir doch legitim, von VO zu sprechen, andere Vorgehensweisen haben auch ihren Namen.

    Den verlinkten Artikel von Heise fand ich nur unnötig hochtrabend (er erklärt auf vier Seiten, was man auch auf einer erklären könnte, an einem sofort nachvollziehbaren Beispiel) und zudem eben wie der Begriff DDD zu sehr danach klingend, als würde etwas revolutionär Neues vorgestellt werden.

    Reply
  20. @Claus-Christoph

    Die meine ich nicht. Dummerweise gibt es den Begriff „Value Object“ aus der Java Welt und das Konzept ValueObject aus DDD mit gleichem Namen.

    Value Object Java = DTO bzw. Transfer Object, siehe:
    http://en.wikipedia.org/wiki/Data_transfer_object

    Value objects aus DDD haben eine völlig andere Bedeutung:
    http://martinfowler.com/bliki/ValueObject.html
    (da ist es kurz und knackig erklärt und es wird auch auf den Unterschied zu den „anderen“ VOs hingewiesen)

    Wenn du Eric Evans (den Vater von DDD reden gehört hast, würdest die letzte Behauptung nicht so stehen lassen.
    Ein ganz bescheidener und kluger Mann, der einfach nur etwas mehr in das Verständnis und die Beobachtung von komplexer Software gesteckt hat und daraus Empfehlungen und nützliche Konzepte extrahiert hat. Übrigens hat er auch einige davon 5 Jahre später „refactored“, also neue Bedeutung zugewiesen und auch die Veränderungen und neuen Erfahrungen einflissen lassen.

    Das ist der Unterschied zu dogmatischen oder fast religiösen Hypes. Für mich die gleiche Liga wie Fowler.

    Naja, und VOs als DIE Grundlage von OOP zu betiteln ist, ähm, ziemlicher Quatsch. Wenn du obige Unterschiede erkennst, merkst du, das sie nur Werkzeuge sind um bestimmte Themen (leichter) zu behandeln. Das kann keine Grundlage sein.

    Cheers!

    Reply
  21. @Claus

    Du verwechselst hier zwei gleichnamige, jedoch verschiedene Konzept. Ist auch nicht deine Schuld, dass sie den selben Namen bekommen haben.

    Value Object (Java) = DTO bzw. Transfer Object, siehe:
    http://en.wikipedia.org/wiki/Data_transfer_object

    Value Objects aus DDD haben eine völlig andere Bedeutung:
    http://martinfowler.com/bliki/ValueObject.html
    (da ist es kurz und knackig erklärt und es wird auch auf den Unterschied zu den „anderen“ VOs hingewiesen)

    Wenn du Eric Evans (den „Erfinder“ von DDD) reden gehört hast, würdest die letzte Behauptung nicht mehr so stehen lassen.
    Ein ganz ruhiger, bescheidener und kluger Mann, der einfach nur etwas mehr in das Verständnis und die Beobachtung von komplexer Software gesteckt hat und daraus Empfehlungen und nützliche Konzepte extrahiert hat. Und ganz im Gegenteil keine in Stein gemeiselten Dogmen verbreitet.

    Übrigens hat er auch einige davon 5 Jahre später „refactored“, also neue Bedeutung zugewiesen und auch die Veränderungen und neuen Erfahrungen einfleissen lassen, auch selbstkritisch erkannt, was er heute anders erklären würde und welche der Konzepte wichtiger und welche unwichtiger geworden sind.

    Das ist der Unterschied zu dogmatischen oder fast religiösen Hypes. Für mich spielt er in der gleichen Liga wie Fowler.

    Naja, und VOs (egal welche der beiden) als DIE Grundlage von OOP zu betiteln ist, ähm, ziemlicher Quatsch. Wenn du obige Unterschiede erkennst, merkst du, dass sie nur Werkzeuge sind um bestimmte Themen (leichter) zu behandeln. Das kann keine Grundlage sein.
    Das wäre wie ein Rührbesen, der die Grundlage des Konditorhandwerks darstellt 😉

    Cheers!

    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