© Ozz Design/Shutterstock.com
Best Practices für Typsicherheit

Teststrategie mit Hilfe der statischen Analyse


Wenn wir eine Anwendung entwickeln, ist es unser Ziel, dass die Software das tut, was sie tun soll. Gleichzeitig muss die Anzahl der Fehler und Ausfälle so klein wie möglich bleiben. Gegen äußere Umstände, die diesem Ziel entgegenstehen, etwa kurze Deadlines oder sich verändernde Anforderungen, sollten wir uns absichern. Helfen kann dabei die Nutzung eines Typsystems im Verbund mit einem statischen Analysetool.

Dieser Artikel beleuchtet das Konzept der Typsicherheit und zeigt, wie es die Zuverlässigkeit und Stabilität des Codes verbessert. Sobald Ihr Code typsicher ist und diese Tatsache durch automatisierte Tools verifiziert wurde, können Sie selbst entscheiden, welche Teile einer Anwendung umfangreiche Unit-Tests benötigen und wo Sie sich auf klar definierte Typen verlassen können.

Typsystem – warum eigentlich?

Mit einem Typsystem kommuniziert man klar und deutlich, welche Arten von Werten durch den Quellcode transportiert werden. Ob nun int, float, string oder boolean – klar ist, dass wir nicht alle Werte gleich behandeln können. Deshalb gilt: Je mehr wir über die Werte wissen, die durch den Code wandern, desto besser. Wer bisher noch keine Typhinweise eingesetzt hat, kommt durch das Hinzufügen von Informationen, ob eine Variable nun int, float, string oder boolean als Wert akzeptiert, schon sehr weit. Wenn nun aber eine Funktion beispielsweise nur Integers akzeptiert, gilt das wirklich für jeden Integer? Oder nur für positive ganze Zahlen? Oder ist nur eine begrenzte Menge von Werten sinnvoll, wie Stunden an einem Tag oder Minuten in einer Stunde?

Eines ist offensichtlich: Mögliche Eingaben auf sinnvolle zu beschränken, reduziert unerwünschtes Verhalten. Wer diesen Ansatz verfolgt, kommt schnell zu der Idee, die eigenen Objekte mit Typhinweisen zu versehen. Die Vorteile liegen auf der Hand: Wir wissen dadurch nicht nur, was wir einer Funktion übergeben können, sondern auch, welche Operationen (Methoden) ein Objekt anbietet. Dabei will ich nicht behaupten, dass skalare Werte niemals ausreichen und stattdessen immer Objekte verwendet werden sollten. Doch sollte man sich immer fragen, wenn man dabei ist, einen String zu typisieren, ob nicht bei der Eingabe etwas schiefgehen könnte. Möchte ich einen leeren String zulassen? Was ist mit Nicht-ASCII-Zeichen?

Anstatt nun aber Validierungslogik in eine Funktion zu packen, die etwas mit einem skalaren Wert macht, ist es besser, ein spezielles Objekt zu erzeugen und die Validierungslogik in seinen Konstruktor zu setzen. Dadurch muss die Validierungslogik nicht immer wieder neu geschrieben werden, und auch das Verhalten der Funktion muss nicht auf ungültige Eingaben getestet werden, da das Objekt mit ungültigen Werten erst gar nicht erstellt werden kann. Nehmen wir als Beispiel eine Funktion, die eine Zeichenfolge für eine E-Mail-Adresse akzeptiert. Wir müssen prüfen, ob die E-Mail gültig ist. Das könnte wie in Listing 1 gezeigt aussehen.

Listing 1

function sendMessage(string $email) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new \InvalidArgumentException( "Invalid email string" ); }  // do something }

Wir können die Aufgabe aber auch anders lösen: Wir schreiben die Funktion so, dass sie explizit ein EmailAddress-Objekt erwartet (Listing 2). So profitieren auch andere Funktionen von der Typisierung.

Listing 2

class EmailAddress { /** @var string */ private $email; public function __construct(string $email) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new \InvalidArgumentException( "Not a valid email string" ); } $this->email = $email; } public function getAddress(): string { return $this->email; } } function sendMessage(EmailAddress $email) {  // do something }

Wenn der Quellcode auf diese Art mit Typhinweisen gefüllt ist, wissen IDEs und statische Analysetools mehr über den Code und unterstützen Sie beim Entwickeln. Wenn beispielsweise eine Property mit phpDoc annotiert wird (leider gibt es noch keine native Unterstützung für Property Types), können Tools verifizieren,

  • ob der Typhinweis valide ist und die Klasse existiert

  • ob nur Objekte dieses Typs zugewiesen werden

  • ob nur Methoden aufgerufen werden, die in der typisierten Klasse verfügbar sind.

 /**  * @var Address  */ private $address;

Dieselben Vorteile, die sich aus Typhinweisen ergeben, gelten auch für Funktions- und Methodenparameter sowie für Rückgabetypen. Es gibt immer den schreibenden Teil (was von einer Methode zurückgegeben wird) und den lesenden Teil (was der Aufrufer mit dem zurückgegebenen Wert macht).

Achten Sie auf die Typen

Typen können Entwicklern auch subtiles Feedback zum grundsätzlichen Design einer Anwendung geben, und man sollte lernen, es zu beachten. Wenn man bei der Implementierung eines Interface beispielsweise gezwungen ist, bei der Hälfte der Methoden, die man einer Klasse hinzufügt, eine Exception zu werfen, ist das Interface wahrscheinlich nicht gut designt. Meist ist man dann gut beraten, das Interface in mehrere kleine aufzuteilen. Die Verwendung eines solchen, schlecht designten Interface im Produktionscode ist gefährlich. Durch die Implementierung gibt man das Versprechen ab, dass die Weitergabe des Objekts im typisierten Interface sicher ist. Der Aufruf seiner Methoden kann aber zu unerwarteten Exceptions führen.

Ein anderes Problem ist die Verwendung von Informationen, die dem Typsystem nicht bekannt sind. Das ist beispielsweise der Fall, wenn ein Entwickler eine Bedingung im Voraus prüft oder er den Rückgabewert einer Methode bereits kennt und dieses implizite Wissen für die weitere Entwicklung nutzt. Tools können an dieser Stelle False Positives produzieren:

if ($user->hasAddress()) { // getAddress() return typehint is ?Address $address = $user->getAddress(); // potentially dangerous - $address might be null echo $address->getStreet(); }

Es gibt keine maschinenlesbare Verbindung zwischen hasAddress() und getAddress() in den Typhinweisen. Das Wissen, dass der obige Code immer funktioniert, ist nur im Kopf des Entwicklers oder durch genaues Betrachten des Quellcodes der Klasse nachzuvollziehen. Man könnte einwenden, dass dieses Beispiel zu einfach ist und jeder versteht, was hier vor sich geht. Aber es gibt viel komplexere Beispiele als dieses in der freien Wildbahn.

Man stelle sich ein Objekt Product vor, bei dem alle Properties nullable sind, weil sie während der Konfigurierung im Content-Management-System leer sein können. Sobald ein Product allerdings veröffentlicht und auf der Website zum Kauf verfügbar ist, müssen die Properties garantiert ausgefüllt sein. Jeder Code, der nur mit veröffentlichten Produkten funktioniert, muss den Check $value !== null durchführen, um dem Typsystem zu entsprechen. Eine Lösung für dieses Problem besteht im Allgemeinen darin, Objekte nicht für ...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang