Mittwoch, 20. Oktober 2010

Time to talk about the contracts

Java EE 5 hat den Wandel von einem schwergewichtigen zu einem leichtgewichtigen Standard vollzogen. Die vielen nützlichen Funktionalitäten des Java EE Standards, die wesentlich von der Java Community geprägt worden sind, erlauben es, Java EE Anwendungen sehr viel einfacher als in früheren Versionen des Standards zu implementieren. Trotz des Optimismus ist dennoch Vorsicht geboten!

Warum ist der Java EE Standard wichtig? Die Frage ist einfach zu beantworten: Der Java EE Standard definiert ein transparentes  und verlässliches Verhalten für Geschäftskomponenten zur Laufzeit und im Fehlerfall. Die Spezifikationen des Java EE Standards beinhalten die einzuhaltenden Verträge, um das definierte Verhalten aus Sicht des Applikationsservers einhalten zu können. Diese Verträge sind essentiell und führen beim nicht Einhalten wiederum zu undefinierten Verhaltensweisen.

Der Java EE Standard definiert im Detail das Transaktionsverhalten bei einem Fehler. Laufzeitausnahmen (Unchecked Exceptions) führen zu einem Rollback bei Container Managed Transactions (CMT), Applikationsausnahmen (Checked Exceptions) hingegen nicht. Ein Rollback kann darüber hinaus über den Session Context einer EJB mit der Methode setRollbackOnly ausgelöst werden. Eine praxistaugliche Möglichkeit ein Rollback zu veranlassen und dennoch eine Applikationsausnahme zu werfen ist die folgende Annotation: @ApplicationException(rollback = true) an einer Applikationsausnahme. Warum eine Applikationsausnahme für den Client im Fehlerfall häufig günstiger ist, als eine Laufzeitausnahme, soll zum Nachdenken anregen. Der Auftritt einer Ausnahme auf der Clientseite ist jedenfalls definiert und in der EJB Spezifikation haarklein erläutert.

Keine Spekulation im Fehlerfall und kein Trail-and-Error bei der Implementierung, sondern ein klar spezifiziertes und nachvollziehbares Verhalten, das zeichnet den Java EE Standard aus!

Verträge sind nicht nur bei Java EE Applikationen essentiell, sondern ebenfalls in der Programmiersprache Java tief verwurzelt. Jeder professionelle Java-Entwickler kennt beispielsweise den equals Vertrag.

Vertrag der equals Methode:
  1. Reflexiv, für jede nicht Null-Referenz liefert a.equals(a) true
  2. Symmetrisch, für jede nicht Null-Referenz liefert a.equals(b) true wenn b.equals(a) true liefert
  3. Transitiv, für jede nicht Null-Referenz und wenn a.equals(b) und b.equals(c) true liefern, muss auch
    a.equals(c) true liefern
  4. Konsistent, für jede nicht Null-Referenz muss die equals Methode konsistent true oder false liefern
  5. Für einen Null-Referenz a.equals(null) muss die equals Methode false liefern
Zerbricht die equals Methode eines Objektes, so betrifft dies nicht nur das Objekt selbst, sondern auch die Verwender des Objektes. Wird die equals Methode überschrieben, ist auch die hashCode Methode zu überschreiben. Dies sind grundlegende Verträge, die Java zwingend vorschreibt. Viele weitere Verträge der Programmiersprache Java sind einzuhalten. Ein Vertragsbruch wird nicht immer durch den Compiler angezeigt und hat häufig sporadische Laufzeitfehler zur Folge. Unit-Tests und Plugins wie PMD, FindBugs und Checkstyle helfen, die Verträge von Java einzuhalten, lösen aber auf der anderen Seite nicht alle potentielle Vertragsbrüche auf. 

Ein Vertragsbruch ist nicht immer nur syntaktischer Natur, sondern kann auch durch die Semantik einer Applikation entstehen. Häufig spielt die Nebenläufigkeit eine Rolle. Weitergehende Techniken im Bereich der Serialisierung und Reflections können zum Vertragsbruch und zur Fragilität einer Anwendung führen. Grundlegende Techniken der objektorientierten Programmierung, wie beispielsweise die Vererbung können ebenfalls zur Fragilität beitragen. Fallgruben lauern überall und nicht jede Fallgrube ist im dichten Netz der Java API offensichtlich.

Java Brainteaser:

Warum sollen in einem Konstruktor (weder direkt noch indirekt) keine überschriebenen Methoden aufgerufen werden?

Verträge von Java und Java EE sind strikt einzuhalten. Darüber hinaus sind die Menge der Java Idiome und Prinzipien ein gutes Mittel, um saubere Java Software zu schreiben.

Seit Java 5 (Tiger Release) sind die Java Generics ein Teil der Syntax. Generics wurden von der Java Community gefordert, weil die Anwendung der Java Collections nicht typsicher war. Die Entwickler von Java gingen bei den Collections zunächst nach dem KISS Prinzip vor und haben es dem Anwender der Programmiersprache überlassen, sich um die Typsicherheit zu kümmern. In der Praxis haben sich dabei Laufzeitprobleme eingestellt (in der Regel ClassCastExceptions), die durch die Generics mit höherer Typsicherheit wesentlich eingegrenzt wurden.

Generics verwenden intern das Prinzip von "erasure", dabei werden Typparameter zur Kompilierzeit durch Casts ersetzt. Der Byte-Code ist deshalb nicht anders als bei Collections, die keine Generics verwenden. Diese Implementierung erlaubt es Generics mit Legacy Collection Quellcode zu vermischen ohne dabei Kompatibilitätsprobleme hervorzurufen. Der Nachteil dabei ist allerdings, dass die Typsicherheit der Generics durch den Programmierer bei der Anwendung von "raw Collections" gebrochen werden kann.

Bei der Anwendung von Generics sind Wildcards gebräuchlich. Wildcards erlauben die Substitution von Datentypen, sodass in einer generischen Collection nicht nur ein einziger Datentyp gespeichert werden kann.

Wildcard-Prinzip:
  1. Nur Lesen, verwende ? extends T
  2. Nur Schreiben, verwende ? super T
  3. Schreiben und Lesen, verwende T (keine Wildcards)
Dieses Prinzip ist durch das Studieren der Generics mit entsprechender Ableitung ermittelbar. Die Menge an Java Idiomen und Prinzipien sind in Kombination mit dem Einhalten der Java Verträge in der Summe die wesentliche Voraussetzung für korrekte Software die einem hohen Qualitätsstandard entspricht.

Read, read, read, know the contracts, use Java Idioms and write Unit-Tests to be on the safe side!