Heiner KückerDesign 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. |