Design by Contract mit Groovy - JAXenter

Design by Contract mit Groovy

Klasseninvarianten mit @Invariant

In dem angegebenen ItemList-Beispiel wurden bereits die beiden Annotationen @Requires und @Ensures für die Angabe einer Vor- und Nachbedingung verwendet. Die dritte und letzte Annotation in GContracts ist @Invariant. Diese Annotation wird für die Angabe von Klasseninvarianten verwendet. Eine Klasseninvariante definiert jene Bedingungen, die über die gesamte Lebenszeit eines Objekts erfüllt sein müssen und somit Bestandteil jeder Vor- und Nachbedingung (gilt auch für nicht explizit formulierte) werden (Listing 2).

Listing 2
@Invariant({ item != null && days.from 

Objekte der Klasse ItemRecord gelten aufgrund der Klasseninvariante nur dann als gültig, wenn das zugewiesene Item vorhanden (nicht null) und die Zeitspanne days valide ist. Die Klasseninvariante gilt während der gesamten Lebenszeit von ItemRecord-Objekten und wird an folgenden Zeitpunkten zur Laufzeit des Programms überprüft:

  • Nach der erfolgreichen Ausführung von öffentlichen Konstruktoren
  • Vor und nach jedem Aufruf einer öffentlichen Instanzmethode
  • Vor und nach jedem Aufruf einer Groovy-Eigenschaft

Klasseninvarianten haben noch eine weitere interessante Eigenschaft in GContracts: sie werden vererbt. Definiert eine Klasse A eine Klasseninvariante Ia und leitet Klasse B von A ab, so wird die Klasseninvariante von Ia automatisch an B weitervererbt. Besitzt B selbst eine Invariante Ib, so wird diese über ein boolesches AND mit Ia verknüpft: Ia AND Ib.

Bei jeder Überprüfung der Invariante Ib wird somit automatisch die Invariante der Klasse A mit einbezogen. Man spricht in diesem Fall von einer Verstärkung der Klasseninvariante in der Subklasse B.

@Ensures und Closure-Parameter

Für die Definition von Nachbedingungen kann es notwendig sein, auf den Rückgabewert der Methode zurückzugreifen. GContracts injiziert den Rückgabewert in den Closure-Parameter result, wenn dieser explizit angegeben wird (Listing 3).

Listing 3
@Invariant({ item != null && days.from  result >= 0 })
    def int days_till_end()  {
      return days.to - new Date()
    }
    // ...
}

Des Weiteren hat es sich als nützlich erwiesen, in der Nachbedingung auf jene Werte der Instanzvariablen zurückzugreifen, die vor der Ausführung einer Methode existierten. Dieser Mechanismus funktioniert nur für Instanzvariablen, die Wertklassen aus dem GDK und JDK darstellen, also bspw.: Integer, Date, GString, String, BigDecimal, etc. (Listing 4).

Listing 4
@Invariant({ description != null && description.size() > 0 })
class Item {
    // ...
    @Ensures({ old -> old.description != description })
    def renew_description(String other)  {
      this.description = other
    }
     // ...
}

Die beiden speziellen Closure-Parameter result und old können in Kombination und in beliebiger Reihenfolge als Closure-Parameter angegeben werden.

Groovy wurde mit Version 1.6 um die Möglichkeit von AST- (Abstract-Syntax-Tree-)Transformationen erweitert [5]. Eine Klasse kann mit der Annotation @GroovyASTTransformation und dem Interface ASTTransformation (beide aus dem Package org.codehaus.groovy.transform) als AST-Transformation markiert werden. Somit wird diese während der Kompilierung instantiiert und zu bestimmten Phasen während des Kompilierprozesses aufgerufen. AST-Transformationen sind also eine Möglichkeit, den AST und somit den zu generierenden Bytecode während der Kompilierung in beliebiger Form zu modifizieren (Listing 5).

Listing 5
@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
public class ContractValidationASTTransformation implements ASTTransformation {

    public void visit(ASTNode[] nodes, SourceUnit unit) {
        final ModuleNode moduleNode = (ModuleNode)nodes[0];

        // ...
    }
}

AST-Transformationen werden in GContracts verwendet, um zur Kompilierung die angegebenen Bedingungen aus den Closure-Parametern der Annotationen @Requires, @Ensures oder @Invariant zu lesen und Java Assertion Statements zu generieren, die an passenden Stellen im AST eingebunden werden. Da seit Java 1.4 Assertionen prinzipiell durch das Schlüsselwort assert unterstützt werden, kann somit während der Kompilierung der generierte Byte-Code erweitert werden.

So führt die aus Listing 1 angegebene Vorbedingung zu folgendem Bytecode-Ergebnis, das zum leichteren Verständnis in Groovy-Code angegeben wird:

class ItemList { 
    // ...
    def void borrow_item(Item item, Date from, Date to)  {
       assert (item != null && !is_borrowed(item))
      itemRecords.add(new ItemRecord(item, from, to))
    }
    // ...
}

Die Verwendung von Java-Assertionen hat vor allem den Vorteil, dass die hierfür vorgesehenen Konfigurationsoptionen der Java VM benutzt werden können. Des Weiteren sind in Java Assertionen standardmäßig nicht aktiviert, und durch die direkte Unterstützung der Java VM auf Bytecode-Ebene entstehen auch keine Performanznachteile.

Tabelle 1: Java-Kommandozeilenparameter für Assertionen

Kommandozeile Bedeutung
java -enableassertions … Aktiviert alle Assertionen
java -ea … Aktiviert alle Assertionen
java -ea:com.ast.domain Aktiviert Assertionen im Package com.ast.domain
java -ea:com.ast.domain.ItemList Aktiviert Assertionen in der Klasse com.ast.domain.ItemList
Java -ea -da:com.ast.controller Aktiviert alle Assertionen außer im Package com.ast.controller
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.