Microsoft Research hat mit Code Contracts ein Konzept vorgestellt, dass die Validierung von Code vereinfachen soll. ZDNet zeigt, was dahinter steckt und wie man mit Code Contracts arbeitet.
Code Contracts[1] ermöglichen es, Validierungslogik in seine Methoden und Klassen einzubinden, ohne viele if/then-Anweisungen schreiben zu müssen. Außerdem bieten Code Contracts die Möglichkeit, diese Validierung sowohl während der Kompilierung als auch zur Laufzeit durchzuführen, was traditioneller Validierungscode nicht schafft.
Was macht Code Contracts so hilfreich für die Validierung? Bislang erforderte Validierungscode vom Entwickler das Schreiben einer großen Anzahl von if/then-Anweisungen. An sich stellt dies kein Problem dar, denn solche Anweisungen sind nicht schwer zu schreiben. Das Problem ist die dahinterstehende Logik, die auf den ersten Blick nicht sofort ersichtlich wird. Es ist schwierig, zwischen Validierungscode und dem eigentlichen Code zu unterscheiden.
Obwohl der Code nicht schwer ist, muss er geschrieben und mühsam gepflegt werden. Von daher wird er nur selten aktualisiert oder sogar ganz vergessen, wenn sich die Geschäftslogik ändert. Mit dem Validierungscode etwas anderes zu tun, als Exceptions (Ausnahmefehler) auszulösen, erfordert viel Arbeit. Daher begnügen sich die meisten Entwickler genau damit, und der Aufrufer registriert diese einfach oder protokolliert sie wie jede andere Fehlermeldung auch. Schließlich ist dieser Code für den Compiler nicht von anderem Code zu unterscheiden, was eine statische Analyse oder spezielle Unit-Tests eher unwahrscheinlich macht.
Zur Verdeutlichung stelle man sich eine Methode mit der folgenden Signatur vor:
Die Geschäftslogik zur Validierung dieser Methode ist, dass 0 <= Input1 <= 100 gilt, Input2.Length > 5 ist, und Input3 den Wert 500 enthalten muss. Der folgende Beispielcode A zeigt die Validierung hierfür.
Beispielcode A
Das ist fürchterlich viel Code für eine so kleine Aufgabe.
Nun kommt noch eine weitere Bedingung hinzu: Der Rückgabewert dieser Funktion darf nicht negativ sein. Dies erfordert einen weiteren if/then-Block, der Exceptions auslöst (siehe Beispielcode B).
Beispielcode B
Es gibt einige Klassen, die feststellen können müssen, ob sie "gültig" oder bereit sind. Häufig schreibt der Entwickler eine IsValid()-Methode, um die Validierung durchzuführen, und ruft diese am Ende jeder Methode auf, die den Status der Klasse ändert. Dies wird schnell zu einer lästigen Pflicht, und der Aufruf leicht vergessen. Mit Code Contracts kann man all dieses mühselige Schreiben von Code auslassen und stattdessen etwas viel Einfacheres verwenden. Code Contracts definiert eine statische Klasse namens "Contract", die zwei statische Methoden zur Validierung von Eingaben und Ausgaben mitbringt: Contract.Requires() und Contract.Ensures(). Beide sollten ganz am Anfang der Methode platziert werden, und beide erwarten konditionale Ausdrücke als Eingabeparameter.
Es gibt einige Einschränkungen, welche Methoden man innerhalb dieser Aufrufe verwenden darf, sowie einige spezielle Details hinsichtlich Schleifen und Collections. Das Code Contracts User Manual[2] (das erstaunlich gut geschrieben ist, wenn man den Status dieses Projekts bedenkt) liefert hierzu alle Einzelheiten. Optional kann man an Contract.Requires() und Contract.Ensures() auch Text übergeben, der als Fehlermeldung verwendet wird.
Beispielcode C zeigt die gesamte Eingabe- und Ausgabevalidierung von Beispielcode A und Beispielcode B in Kombination.
Beispielcode C
Dieser Code ist wesentlich klarer und kompakter. Und er ist sofort als Validierungscode erkennbar.
Für das Problem, den Objektstatus zu validieren, ermöglicht Code Contracts das Erstellen einer Methode, der man das Attribut [ContractInvariantMethod] hinzufügt. Die Methode darf nur Aufrufe von Contract.Invariant() enthalten, die ebenfalls konditionale Ausdrücke zulässt. Sobald dies erledigt ist, wird der Code in der Methode verwendet, um das Objekt am Ende jedes Aufrufs einer seiner Methoden zu validieren. Visual Studio 2010 Beta 1 wird mit Code Contracts in mscorlib.dll geliefert, sodass der entsprechende Namensraum von Haus aus verfügbar ist. Um jedoch die Vorteile der Integration in Visual Studio nutzen zu können, muss man das Code Contracts-Paket herunterladen und installieren. (Es gibt eine akademische und eine kommerzielle Lizenz, die beide kostenlos sind.)
Als Nächstes sollte sichergestellt sein, dass eine Referenz auf die Code-Contracts-Assembly hinzugefügt ist. Klassen, die Code Contracts verwenden, sollten eine Referenz auf den Namensraum System.Diagnostics.Contracts hinzufügen.
Und schließlich wird man feststellen, dass unter den Projekteigenschaften nun eine Registerkarte für Code Contracts hinzugekommen ist. Auf dieser Eigenschaftenseite kann man Code Contracts für die Laufzeitüberprüfung und statische Analysen aktivieren und deaktivieren. Darüber hinaus lassen sich die Einstellungen dieser beiden Systeme auf der Eigenschaftenseite vornehmen.
Wichtige Hinweise: Die Konfiguration ist mit den Kompilierungseinstellungen verknüpft. Das bedeutet, dass man bestimmte Funktionen für Release Builds selektiv deaktivieren kann, sodass der Produktionscode keinen Code enthält, der noch nicht produktionsreif ist (Code Contracts ist noch kein endgültiges Release). Darüber hinaus benötigt die Validierung auch einiges an Rechenressourcen, und ihr standardmäßiges Verhalten (wie im Folgenden beschrieben) ist in den meisten Produktionsanwendungen nicht erwünscht.
Wer will, kann die Vorteile von Contract.Requires() auch nutzen, ohne irgendwelchen Code unter vielen Umständen schreiben zu müssen. Falls man eine Methode hat, bei der die ersten Anweisungen aus einer Gruppe von if/then/throw-Anweisungen ohne else-Klauseln bestehen, und diese Anweisungen die Anforderungen von Contract.Requires() erfüllen, kann ein Aufruf von Contract.EndContractBlock() nach der Anweisungsgruppe hinzufügen. Code Contracts wird diese Gruppe von Anweisungen als Vorbedingungen behandeln, genauso wie mit Contract.Requires().
Bei Invarianten sollten alle Eigenschaften, die für die Validierung verwendet werden, ausdrücklich deklariert werden. Und der zugrunde liegende Wert (nicht die Eigenschaft) sollte getestet werden. Bei Tests hat sich gezeigt, dass andernfalls die Validierung bereits einsetzt, noch ehe alle erforderlichen Eigenschaften festgelegt wurden. Wenn der Code kompiliert wird, schreibt das Code-Contracts-System (sofern aktiviert) den Code etwas um. Zuerst eine Ausnahme: Wird Contract.Requires() der Text für eine Fehlermeldung übergeben, dann löst das statt des standardmäßigen Verhaltens eine Exception unter Verwendung dieses Textes aus. Doch außer in diesem Spezialfall (und wenn bestimmte Exceptions ausgelöst werden, wie in der Dokumentation beschrieben) ruft der Rewriter bei einem fehlgeschlagenen Aufruf von Contract die Methode ReportFailure() auf. Keine Angst, diese Methode wird automatisch generiert.
Wer will, kann sie jedoch durch eine eigene überschreiben. Die standardmäßige Implementierung löst das Ereignis "RaiseContractFailedEvent" aus. Falls es keinen Handler für dieses Ereignis gibt, wird die Methode TriggerFailure() aufgerufen (auch hier kann man seine eigene Implementierung bereitstellen), die die Anwendung abbricht und die Ausführung an den Debugger übergibt. Das System löst also keine Exceptions aus (um Tester und Entwickler auf den Fehler aufmerksam zu machen), aber falls man diese Funktionalität bevorzugt, ist es trivial, sie bereitzustellen. Alternativ kann man die Laufzeitüberprüfung auch so modifizieren, dass stattdessen Fehler ausgelöst werden. Dafür muss ein entsprechendes Kästchen auf der Code-Contracts-Seite der Projekteigenschaften aktiviert sein.
Statische Analyse
Die statische Analyse von Code Contracts überprüft den Code, wenn er kompiliert wird, und hält nach problematischen Stellen Ausschau. Aus prinzipiellen Gründen kann eine statische Analyse natürlich nicht perfekt sein. Statt zu behaupten, dass der Code nicht funktioniert, meldet sie nur, dass ein Contract "nicht bewiesen" ist. Die entsprechenden Warnungen erscheinen in der "Fehlerliste" von Visual Studio.
Standardmäßig ist die statische Analyse deaktiviert, weil sie ein äußerst rechenintensiver Prozess ist, aber sie lässt sich auf der Eigenschaftenseite aktivieren. Weil die statische Analyse Unmengen von Warnungen erzeugen kann, die nicht unbedingt aussagekräftig sind, erlaubt einem das System, eine XML-Datei mit "Basis"-Fehlern zu definieren. Wenn diese "Basis"-Datei in die Analyse eingebunden ist, werden keine Warnungen über die Komponenten in dieser Datei ausgegeben. Das ist ein nützliches Feature, das es einem ermöglicht, sicherzustellen, dass nur neue, unbekannte Warnungen zu sehen sind.
URLs in diesem Artikel:
[1] = http:/
[2] = http:/