Wie man Daten validiert

Wie man Daten validiert

Die Validierung von Daten scheint eine der wichtigsten Aufgaben einer Anwendung zu sein. Schließlich kann man Daten aus externen Quellen nicht vertrauen. Werfen wir also einen Blick darauf, wie man die Datenvalidierung effizient implementiert.

Nehmen wir an, wir benötigen ein Profil, das einige benutzerbezogene Daten enthält. Wir werden klein anfangen und die Validierung im ersten Schritt ignorieren. Idealerweise können wir ein Objekt über seinen Konstruktor initialisieren:

class   Profile
{
     private   $firstName ;
     private   $lastName ;
     private   $email ;
     private   $nickname ;
     private   $homepage ;
 
     public   function   __construct (
         $firstName ,
         $lastName ,
         $email ,
         $nickname ,
         $homepage
     )
     {
         $this -> firstName   =   $firstName ;
         $this -> lastName    =   $lastName ;
         $this -> email       =   $email ;
         $this -> nickname    =   $nickname ;
         $this -> homepage    =   $homepage ;
     }
 
     // ...
}
 
$profile   =   new   Profile (
     'John' ,
     'Doe' ,
     'user@example.com' ,
     'johnny' ,
     'http://example.com'
) ;

Die Aufgabe des Konstruktors ist es, ein Objekt in einen vernünftigen Zustand zu initialisieren. Zugegeben, wir haben noch nichts validiert, sondern nur die Parameter gespeichert. Wir können also noch nicht sagen, ob sich das Objekt in einem gesunden Zustand befindet oder nicht. Dazu kommen wir in einer Minute.

Optionale Parameter

Nicht jeder Konstruktor hat eine so schöne Signatur wie die oben gezeigte. Die Dinge neigen dazu, bei vielen optionalen Parametern ein wenig unübersichtlich zu werden:

class   Profile
{
     // ...
 
     public   function   __construct (
         $lastName ,
         $firstName   =   null ,
         $email   =   null ,
         $nickname   =   null ,
         $homepage   =   null
     )
     {
         // ...
     }
}
 
$object   =   new   Profile ( 'Doe' ,   null ,   null ,   null ,   'http://example.com' ) ;

Ich mag diese Methodensignatur nicht. Sie macht den Code schwer lesbar und anfällig für Fehler. Und wer zählt schon gerne null Werte? Um die optionalen Konstruktorparameter zu umgehen, können wir Setter für alle optionalen Parameter erstellen:

class   Profile
{
     // ...
 
     public   function   __construct ( $lastName )
     {
         $this -> lastName   =   $lastName ;
     }
 
     public   function   setFirstName ( $firstName )
     {
         $this -> firstName   =   $firstName ;
     }
 
     public   function   setEmail ( $email )
     {
         $this -> email   =   $email ;
     }
 
     // ...
}

Wann und wo validieren wir nun? Wir könnten eine validate() Methode erstellen, die ein Array oder eine Sammlung von Fehlermeldungen zurückgibt, wenn die Validierung fehlgeschlagen ist. Ich habe diesen Ansatz ziemlich oft gesehen:

class   Profile
{
     // ...
 
     public   function   validate ( )
     {
         $errors   =   [ ] ;
 
         // add error if last name is not empty
         // add error if email address is invalid
         // ...
 
         return   $errors ;
     }
 
     // ...
}

Dieser Ansatz ist extrem gefährlich: Bevor die Methode validate() aufgerufen wurde, können Sie nicht sagen, ob sich das Objekt in einem gültigen oder ungültigen Zustand befindet. Noch schlimmer: Die Methode validate() wird vielleicht nie aufgerufen. Außerdem hat das Objekt Setter, so dass sich sein Zustand nach der Validierung ändern könnte. Bedenken Sie folgendes:

class   Collaborator
{
     private   $profile ;
 
     public   function   setProfile ( Profile   $profile )
     {
         if   ( count ( $profile -> validate ( ) )   !=   0 )   {
             // bail out
         }
 
         $this -> profile   =   $profile ;
     }
}
 
$profile   =   new   Profile ( 'Doe' ) ;
$profile -> setEmail ( 'user@example.com' ) ;
 
$errors   =   $profile -> validate ( ) ;
 
if   ( count ( $errors )   !=   0 )   {
     // bail out
}
 
$collaborator   =   new   Collaborator ;
 
$collaborator -> setProfile ( $profile ) ;

Wir erstellen ein Profile-Objekt, geben eine gültige E-Mail-Adresse ein und überprüfen das Profil. Wenig überraschend stellt sich heraus, dass es gültig ist, also übergeben wir eine Referenz an einen Mitarbeiter. Der Collaborator validiert das Profil sogar erneut, da er nicht sicher sein kann, ob das Profil gültig ist oder nicht. Da das Profil gültig ist, behält der Collaborator einen Verweis auf das Profil.

Nun geschieht Folgendes außerhalb von Collaborator:

$profile -> setEmail ( 'not-a-valid-email-address' ) ;

Das Profile ist soeben ungültig geworden, und der Collaborator hält nun einen Verweis auf ein ungültiges Objekt. Unser Universum ist gerade zusammengebrochen. All unsere Bemühungen waren umsonst, denn wir haben es möglich gemacht, die Validierung zu umgehen und sie damit nutzlos zu machen.

Ich habe solchen Code in freier Wildbahn schon viel zu oft gesehen. Einige Frameworks scheinen dies sogar als Best Practice vorzuschlagen, manchmal sogar mit einem "ausgefeilteren" Weg, die Validierung durchzuführen:

class   Profile
{
     // ..
 
     public   function   validate ( Validator   $validator )
     {
         return   $validator -> validate ( $this ) ;
     }
 
     // ...
}

Ein Ansatz wie dieser würde es Ihnen ermöglichen, Validierungsregeln in eine Konfigurationsdatei zu schreiben und sie durch die Magie des Frameworks auszuführen. Das trennt die Validierung vom eigentlichen Objekt und macht es zu einem dummen Datencontainer. Das löst jedoch keines unserer Probleme: Das Objekt kann immer noch zu jedem beliebigen Zeitpunkt ungültig werden.

Ungültige Objekte

Das Hauptproblem bei diesem Ansatz ist, dass wir einem Objekt überhaupt erst erlauben, in einen ungültigen Zustand zu kommen. Das ist eine Todsünde, denn es zwingt uns zurück in die prozedurale Programmierung, da wir Objektreferenzen nicht sicher weitergeben können.

Es darf nicht möglich sein, dass ein Objekt einen ungültigen Zustand annimmt. Wir müssen in der Lage sein, Referenzen darauf weiterzugeben. Und wenn wir nicht wissen, ob wir eine Referenz auf ein gültiges oder ein ungültiges Objekt halten, können wir uns nicht auf das Objekt verlassen. Selbst wenn wir das Objekt jedes Mal neu validieren, wenn wir mit ihm arbeiten, was sollen wir mit den Fehlermeldungen machen, die wir zurückbekommen? Diese Fehlermeldungen sind dazu da, dem Benutzer eine Rückmeldung zu geben, und irgendwo tief in unserem Objektgraphen können wir diese Meldungen nicht einmal an den Benutzer zurückgeben.

Können wir dieses Problem beheben, indem wir das Profile-Objekt selbst die Validierung ausführen lassen?

class   Profile
{
     // ...
 
     public   function   __construct (
         $lastName ,
         $firstName   =   null ,
         $email   =   null ,
         $nickname   =   null ,
         $homepage   =   null ,
         Validator   $validator
     )
     {
         // ...
 
         return   $validator -> validate ( $this ) ;
         // this does not work!
     }
 
     // ...
}

Das funktioniert nicht, weil Konstruktoren keine Werte zurückgeben können. Wir könnten bei fehlgeschlagener Validierung eine Exception werfen, aber wie würden wir dann die Fehlermeldungen zurückmelden?

Macht nichts: Wir erinnern uns, dass wir bereits auf Setter-Methoden umgestiegen waren, um das Objekt zu initialisieren:

class   Profile
{
     // ...
 
     public   function   __construct ( $lastName ,   Validator   $validator )
     {
         $this -> lastName    =   $lastName ;
         $this -> validator   =   $validator ;
 
         return   $this -> validator -> validate ( $this ) ;
         // this still does not work!
     }
 
     public   function   setFirstName ( $firstName )
     {
         $this -> firstName   =   $firstName ;
 
         return   $this -> validator -> validate ( $this ) ;
     }
 
     public   function   setEmail ( $email )
     {
         $this -> email   =   $email ;
 
         return   $this -> validator -> validate ( $this ) ;
     }
 
     // ...
}

Aus der Sicht des Setters würde dies funktionieren. Aber es fühlt sich so an, als würden wir die gleiche Validierung immer und immer wieder wiederholen. Wenn wir nur den ersten Namen ändern, warum sollten wir alles andere erneut validieren - es kann sich ja nicht geändert haben. Außerdem haben wir immer noch das gleiche Konstruktor-Problem: Auch wenn es nur einen obligatorischen Parameter gibt, können wir die Fehlermeldungen nicht zurückmelden. Wir müssten also auch einen Setter für den Nachnamen schreiben und den Konstruktor leer lassen.

Das eigentliche Problem bei diesem Ansatz ist jedoch, dass uns die Liste der Fehlermeldungen entgeht. Es gibt keine einzige Methode mehr, die uns diese Liste liefern kann. Keine Sorge, die bekommen wir wieder.

Es stellt sich heraus, dass wir die Validierung in kleinere Teile zerlegt haben, nämlich in die Validierung der einzelnen Felder. Nun, in diesem Fall können wir den Code vereinfachen:

class   Profile
{
     private   $lastName ;
 
     // ...
 
     public   function   __construct ( $lastName )
     {
         $this -> setLastName ( $lastName ) ;
     }
 
     private   function   setLastName ( $lastName )
     {
         if   ( $lastName   ==   '' )   {
             throw   new   InvalidArgumentException ( 'Last name required' ) ;
         }
 
         $this -> lastName   =   $lastName ;
     }
 
     public   function   setFirstName ( $firstName )
     {
         $this -> firstName   =   $firstName ;
     }
 
     public   function   setEmail ( $email )
     {
         // throw exception when email address is invalid
 
         $this -> email   =   $email ;
     }
 
     // ...
}

Wenn wir versuchen, ein Profil mit einem leeren Nachnamen zu erstellen, wird die Methode setLastName() eine Ausnahme auslösen. Aus der Sicht des Profils ist das korrekt: Es ist eine Geschäftsregel, dass ein Profil einen nicht leeren Nachnamen benötigt, also können Sie kein Profil mit einem leeren Nachnamen erstellen. (Zum Glück haben wir das Attribut $lastName privat gemacht!)

Beachten Sie, dass die Methode setLastName() privat ist, denn sobald das Objekt erstellt ist, gibt es (hoffentlich) keinen Grund, den Nachnamen jemals zu ändern. Wenn es einen geschäftlichen Grund gäbe, den Nachnamen zu ändern, könnten wir den Setter öffentlich machen. Wie auch immer, wir können diesen Setter nicht umgehen (zumindest nicht von außerhalb des Objekts). Eigentlich sehe ich dies eher als eine "Geschäftsregel" als eine "Validierungsregel" an.

Für mich fängt der Code an, ansprechender auszusehen: Wir haben unseren großen magischen Validator aufgespalten und haben begonnen, Geschäftsregeln explizit im Code darzustellen, anstatt in einer separaten Konfigurationsdatei.

Die Validierung der E-Mail-Adressen braucht allerdings noch etwas Arbeit. Bis jetzt zeige ich noch nicht einmal den eigentlichen Code, was zum Teil daran liegt, dass man in Bezug auf die Validierung von E-Mail-Adressen nicht wirklich viel machen kann: Lesen Sie die relevanten RFCs, um eine Vorstellung davon zu bekommen, wie viele verschiedene Zeichenketten gültige E-Mail-Adressen darstellen. Vielleicht ist es nicht wirklich sinnvoll, einfach einen regulären Ausdruck von irgendwo aus dem Internet zu kopieren und einzufügen, um das zu validieren.

E-Mail-Adressen

Wie wäre es zum Beispiel mit root@127.0.0.1? Dies ist eine gültige E-Mail-Adresse. Ihre Anwendung könnte jedoch entscheiden, sie nicht zu akzeptieren, weil der Product Owner die Entscheidung getroffen hat, dass nur E-Mail-Adressen mit Domänennamen als gültig angesehen werden und IP-Adressen nicht akzeptiert werden. Verstehen Sie, was hier passiert? Der Unterschied zwischen "Geschäftsregel" und "Validierungsregel" ist gerade noch deutlicher geworden. Je spezifischer unsere Regeln werden, desto weniger Hilfe können wir von einem generischen Validator erwarten.

Aber wie sieht es mit Code-Duplizierung aus? Validatoren sind in erster Linie dazu da, Code-Duplikation zu vermeiden, richtig? Lassen Sie uns das obige Beispiel noch einmal betrachten. Vielleicht entscheiden wir uns dafür, sicherzustellen, dass eine E-Mail-Adresse mindestens ein Zeichen vor einem @-Zeichen plus weitere Zeichen und mindestens einen Punkt danach enthält. (Das ist nicht das Beste, was uns einfallen könnte, aber für die Zwecke dieses Beispiels wollen wir es dabei belassen. Wir werden den echten Code sowieso nicht zeigen.)

Lassen Sie uns also diesen Code in unser Profile-Objekt einfügen. Wir werden eine eigene Methode erstellen:

class   Profile
{
     // ...
 
     public   function   setEmail ( $email )
     {
         $this -> ensureEmailAddressIsValid ( $email ) ;
         $this -> email   =   $email ;
     }
 
     private   function   ensureEmailAddressIsValid ( $email )
     {
         // throw exception when email address is invalid
     }
 
     // ...
}

Das funktioniert gut, bedeutet aber in der Tat Code-Duplizierung, weil wir diese Methode in jedes andere Objekt kopieren müssen, das eine E-Mail-Adresse validieren muss. Aber halt - warum validiert ein Profilobjekt überhaupt eine E-Mail-Adresse? Wir könnten diesen Code in ein separates Objekt verschieben. Warum nennen wir es nicht EmailAddress?

class   Profile
{
     // ...
 
     public   function   setEmail ( EmailAddress   $email )
     {
         $this -> email   =   $email ;
     }
 
     // ...
}
 
class   EmailAddress
{
     private   $email ;
 
     public   function   __construct ( $email )
     {
         $this -> ensureEmailAddressIsValid ( $email ) ;
 
         $this -> email   =   $email ;
     }
 
     private   function   ensureEmailAddressIsValid ( $email )
     {
         // throw exception when email address is invalid
     }
 
     // ...
}

Es lohnt sich, kleine Objekte zu erstellen, die Geschäftsregeln kapseln. Sie sind aus geschäftlicher Sicht sinnvoll. Sie lassen sich leicht wiederverwenden. Und sie helfen, Code-Duplikation zu vermeiden.

Aus der Sicht eines Geschäftsobjekts gibt es das Konzept der Validierung nicht wirklich. Geschäftsobjekte (Objekte, die Dinge repräsentieren, die für Ihr Unternehmen wichtig sind) setzen Geschäftsregeln durch. Sie validieren keine Daten. Business-Objekte treten niemals in einen ungültigen Zustand ein. Sie könnten eine Ausnahme auslösen, wenn wir sie auffordern, ihren Zustand zu ändern und eine der Geschäftsregeln, die sie kapseln, verletzt wird. Wenn also ein Setter (in diesem Fall auch Mutator genannt) aufgerufen wird, ändert das Objekt entweder seinen Zustand oder es wird eine Exception ausgelöst. Es wird aber niemals einen ungültigen Zustand annehmen.

Code-Duplikation

Es stellt sich also heraus, dass Code-Duplizierung kein wirkliches Problem ist, wenn wir kleine und sinnvolle Objekte bauen. Sie sind wiederverwendbar, wodurch doppelter Code vermieden wird. Übrigens: Das Objekt EmailAddress im obigen Beispiel ist ein sogenanntes Wertobjekt. Denken Sie für unser Beispiel an Objekte wie Name (das aus einem Vorname- und einem Nachname-Objekt zusammengesetzt sein könnte) oder ein Homepage-Objekt (das ein allgemeineres URL-Objekt verwenden könnte). Im Grunde können (und sollten) Sie für alles, was aus geschäftlicher Sicht sinnvoll ist, ein Objekt erstellen, zumindest für alles, was mit Regeln verbunden ist.

Aber trotzdem fehlt uns die Liste der Fehlermeldungen, die wir für die Benutzerinteraktion benötigen. Ich werde Ihnen nächste Woche zeigen, wie Sie diese bekommen.