Heiner Kücker

Design by Contract / Programming by Contract

Home

Java-Seite

Alaska-XBase++-Seite

Projekte

Philosophien
Techniken


Konzepte

   Artikelsystematik

   Semantisches_Netz

   Flexible_Columns

   Weiterentwicklung_Java

   Fehlerquellen

   Längencodierung

   Encoding

   Programming_by_Contract

   TimelineStructure
   Datenstruktur für
   Kalender oder
   Terminplaner

Sudoku

Kontakt /
Impressum


Links

SiteMap





Letzte Aktualisierung:
19.02.2003

Design by Contract / Programming by Contract

Design by Contract / Programming by Contract
============================================

Ich möchte hier eine Variante des DbC aufzeigen, die im Gegensatz
zu den anderweitig ausführlich erläuterten Assertions auch zur
Kompilier-Zeit wirkt.

Beispiel
--------

Angenommen, eine Methode habe einen int-Parameter. Dieser
Parameter könnte ein Betriebsartenschalter (0, 1, 2 usw.), ein
nullbasierter Array-Index (0 bis Array.length-1), ein 1-basierter
Index (1 bis Maximum) oder einfach eine ganze Zahl (Wertebereich
positiv und negativ) sein.

Ein weiteres Beipsiel sind Strings als Parameter. Diese können
von einer Artikelnummer, über einen Pfad-Dateinamen bis zum
Freitext alles mögliche bedeuten. Fehler durch Verwechslungen,
die der Compiler nicht prüfen kann, sind hier fast
vorprogrammiert.

Insofern kann man sagen, ein int-Parameter

	public void method(int iParam){

ist uncodiert.

In Java gibt es keine Typisierung für Elementartypen. Deshalb
legen wir entsprechende Objekte an:

/**
 * ganzer positiver Zahlenwert mit Basis Null
 */
public class Base0Int {
  private int iValue;

  /** Verboten */
  private Base0Int(){;}
  /** Konstruktor mit Prüfung der Einhaltung des Kontraktes */
  public  Base0Int(int iParam){
    if (iParam < 0){
      throw new RuntimeException("Contract-Verletzung(noch auf englisch übersetzen)"+this);
    }
    iValue = iParam;
  }//end constructor
}//end class


Weil das Umwandeln(quasi Casten) vom uncodierten Wert zum
Contract-Objekt die eigentliche Prüfebene ist, verwenden wir eine
RuntimeException. Das Erzwingen eines zur weiteren Prüfung
herumzulegenden try-catch´s durch Werfen einer checked-Exception
ist hier nicht gewünscht.

Die Contract-Klassen sind entweder sogennante Immutables (Setzen
der Werte nur im Konstruktor), oder alle set-Methoden müssen
ebenfalls die Einhaltung des Kontraktes prüfen.

int iFoo = ... ; //unchecked
...
Base0Int base0IntFoo = new Base0Int(iFoo); // Casten auf checked
...
XyClass.method(base0IntFoo);//verwenden

Durch das Verwenden der Contract-Objekte als Methoden-Parameter
ist das Durchschlagen der Contractverletzung durch den Methoden-
Aufruf nicht möglich. Die Fehlermöglichkeit wird auf den Code,
der bis zum Casten durchlaufen wird, eingeschränkt.
Konsequenterweise kann man schlussfolgern, dass API-Methoden
(public) nur Contract-Parameter haben sollten.

Die immutablen Contract-Objekte kann man alternativ mit dem
Konstruktor oder über eine Factory erzeugen.

Den Konstruktor verwenden wir bei einem großen Wertebereich und
wenigen Instanzen. Bei einem geringen Wertebereich (Status-
Schalter) bietet sich eine Factory an, da dann die selben wenigen
Instanzen beliebig oft verwendet werden können, was Ressourcen
spart.

Die Verwendung des Enum-Patterns entspricht dieser Herangehensweise.

sinnvoll: Kasten mit Enum-Pattern

Problem Nullpointer
-------------------

Durch die Verwendung von Objekten statt Elementarwerten tritt das
Problem der NullpointerExceptions auf.

Einerseits kann das beim Einsatz der Software zu Abstürzen
führen, andererseits ist das Auftreten einer NullpointerException
bei Debugging und Test ein Hinweis auf die Verletzung des
Contracts, die sonst vielleicht übersehen worden wäre.

Kontrakt-Klassen mit Wertebereichsbezug
---------------------------------------

Unsere Base0Int-Klasse verhindert das Auftreten von Array-Indizes
kleiner Null. Was ist aber bei zu grossen Array-Indizes? Hier
wäre es Wünschenswert, beim Casten das konkret adressierte Array
mit in die Prüfung einzubeziehen.

/**
 * Interface für Klassen, die ein Array, eine ArrayList oder
 * eine andere über Index adressierte Struktur enthalten
 */
public interface ArrayRangeInterface {
  public int getMaxIndex();
}


/**
 * Interface für Klassen, die einen gecheckten Wert darstellen
 */
public interface CheckedValueInterface {
  public Object getRefObject();
}


/**
 * gecheckter ArrayIndex
 */
public class ArrayIndex implements CheckedValueInterface {
  private ArrayRangeInterface ariObject;
  private int iValue;

  /** Verboten */
  private Base0Int(){;}
  /** Konstruktor mit Prüfung der Einhaltung des Kontraktes */
  public  Base0Int(ArrayRangeInterface ariParam, int iParam){
    if (iParam < 0){
      throw new RuntimeException("Contract-Verletzung(noch auf englisch übersetzen) "+this+" less zero");
    }
    if (iParam > ariParam.getmaxIndex()){
      throw new RuntimeException("Contract-Verletzung(noch auf englisch übersetzen) "+this+" greater range "+ariParam+" maxRange "+ariParam.getmaxIndex());
    }
    ariObject = ariParam;
    iValue = iParam;
  }//end constructor

  /** set-Methode (nicht bei Immutablen) mit Prüfung der Einhaltung des Kontraktes */
  public void setIndex(ArrayRangeInterface ariParam, int iParam){
    if (iParam < 0){
      throw new RuntimeException("Contract-Verletzung(noch auf englisch übersetzen) "+this+" less zero");
    }
    if (aiParam.getRefObject() != this){
      //Prüfen auf zutreffenden gecheckten Wert für dieses Objekt
      //Prüfung ist leider nur zur Laufzeit möglich
      throw new RuntimeException("wrong checked value is checked for "+ariParam.getRefObject()+", checked value for "+this+" expected");
    }
    iValue = iParam;
  }//end method setIndex
}//end class


/**
 * Klasse, die eine über Index adressierte Struktur enthält
 */
public class ArrayClass implements ArrayRangeInterface{
  private String[] strArrMember;

//Konstruktor hier weggelassen

  public int getMaxIndex(){
//natürlich darf strArrMember nicht null sein
//Massnahmen hier weggelassen
    retu(strArrMember.length);
  }

  //Methode mit gechecktem Parameter
  //der Parameter wird geprüft ob er gegenüber diesem Objekt gecheckt ist
  //so fällt ein Verwechseln des Parameters für mehrere Arrays(Objekte/Instanzen) sofort auf
  public void method(ArrayIndex aiParam){
    if (aiParam.getRefObject() != this){
      //Prüfen auf zutreffenden gecheckten Wert für dieses Objekt
      //Prüfung ist leider nur zur Laufzeit möglich
      throw new RuntimeException("wrong checked value is checked for "+ariParam.getRefObject()+", checked value for "+this+" expected");
    }
    ...
  }

Die im Codelisting recht umfangreich aussehende Konstruktion
erlaubt die Prüfung des Wertes auf den Wertebereich für das
konkret adressierte Objekt und ausserdem die Laufzeitprüfung, ob
überhaupt das passende Objekt angesprochen wird. Beim Arbeiten
mit mehreren Arrays und entsprechenden Index-Zählern könnte
schnell ein Verwechslen der Arrays/Zähler passieren. Das wird
abgefangen.

Um die Laufzeitprüfung zu sparen, könnte man getrennte Klassen
für die jeweiligen Indexwerte und adressierten Strukturen bauen.


Fazit
-----

Die Beispiele zeigen, wie man Design by Contract auch ohne
Assertions und mit dem zusätzlichen Vorteil der Prüfung zur
Compile-Zeit realisieren kann. Hier wurden lediglich Arrays zur
Erläuterung der Grundidee herangezogen. Das Prinzip ist auf
beliebige Datentypen übertragbar. Bei sinnvoller Namensgebung der
gecheckten Parameter dokumentieren sich diese quasi selbst.
Entstehende Verluste bezüglich Performance und
Ressourcenverbrauch muss man selbst in das Verhältnis zur
gewonnenen Sicherheit setzen. Die höhere Sicherheit steht dabei
meist im Vordergrund. Bei getestem Code kann man die Prüfungen
entfernen, während sie bei bereitgestellten API´s (public-
Methoden) bleiben sollten.

Ein weiteres Problem ist die besonders bei gecheckten Parametern
mit Wertbereichsbezug eingebrachte zusätzliche Komplexität. Beim
Lesen des Quelltextes lenkt der Prüfcode vom fachlichen Code ab.
Deshalb sollte man den Prüfcode in Untermethoden oder bei
Verwendung eines Preprozessors in Codemakros unterbringen.
Richtig gut wären Aspekte, welche die Prüfung realisieren. Die
aspektorientierte Programmierung bietet sicher noch weitere
Verbesserungsmöglichkeiten bezüglich DbC.

Wünschens-/Denkenswerte Spracherweiterungen
-------------------------------------------

Für Elementardatentypen wäre eine Typisierung analog zu C oder
Pascal wünschenswert. Dies wird man in Java wahrscheinlich aus
Prinzip nicht machen.

Bei der extensiven Verwendung von Immutablen wäre ein
entsprechendes Schlüsselwort mit Kompilerprüfung auf verbotene
set-Operationen wünschenswert:

	public immutable class XyImmutableClass

Dies hätte ausserdem eine vorteilhafte selbstdokumentierende Wirkung.

Einmal in Schwung kann man den Bogen zum Einarbeiten von Design-
Pattern in Java ziehen. Vorstellbar ist auch ein Schlüsselwort
für Singleton oder Factory, während komplexere Pattern nicht so
ohne weiteres zu Integrieren sind.

Generator/Preprozessor/AspectOrientierung
-----------------------------------------

Durch Code-Generatoren oder Preprozessoren kann man Features in
Programme einbauen, welche die Programmiersprache eigentlich
nicht hat. Das geht in die Richtung aspectorientierter
Programmierung.

Ich stelle mir vor, dass man in die Sprache Konstrukte einbaut,
elche die Prüfung der Contracts übernehmen.

Beim Betreten eines gesicherten Blockes werden die Kontrakte
geprüft. Damit treten die Laufzeitfehler näher an der
Verursacherstelle auf.

Besser ist natürlich eine Prüfung zur Compile-Time oder in einem
speziellen Prüflauf.

Im Grunde ist es ganz einfach. In der EDV kann alles, was
konstant ist, auch variabel sein. Alles was variabel ist, sollte
geprüft werden. Manche Hilfs-Architekten vergessen das (wenn sie
es jemals wussten).

So sollten mit der Tendenz der Verlagerung von Code in XML-
Dateien oder der Anbindung externer Technologien über formlose
Strings (JDBC) Prüfmechanismen eingeführt werden.