André Krämers Blog

Lösungen für Ihre Probleme

Protagonisten von Kriminalfilmen, die die Weisheit “Traue niemandem” beherzigen, leben länger und gehören am Ende des Films meist zu denjenigen, die nicht tot sind.  Was im Film gut ist, kann in der Softwareentwicklung doch nicht nicht schlecht sein, oder? Genau! Dies ist der Grund, weshalb man aufrufendem Code grundsätzlich misstrauen sollte! Glücklicherweise führt zuwiderhandeln in der Softwareentwicklung zwar in den seltendsten Fällen zum Tod, zu einem Haufen Überstunden kann es aber sehr schnell führen.

Anhand folgenden Beispiels möchte ich dies gerne verdeutlichen:

Unsere Aufgabe ist es, eine Methode zu schreiben, die eine angegebene, temporäre Datei löscht. Im einfachsten Fall könnte dies so aussehen:

class FileHelper
{
  ///<summary>
  /// Deletes the file.
  ///</summary>
  ///<param name="fileName">Please enter valid  (*.tmp)
  /// and existing file names only!</param>
  public static void DeleteTempFile(string fileName)
  {
    System.IO.File.Delete(fileName);
  }
}

Wie wir sehen, wurde ab der Zeile 8 die Methode “DeleteTempFile” implementiert, die in Ihrem Kommentar höflich darum bittet, nur gültige und vor allem nur temporäre Dateinamen zu übergeben.

So freundlich der Kommentar zwar ist, so wenig nützlich ist er aber auch. Dem Aufrufer unserer Methode wäre es ein leichtes, folgendes hinein zu geben:

FileHelper.DeleteTempFile(null);
FileHelper.DeleteTempFile(@"c:\diese\Datei\gibt\es\nicht.hahaha");
FileHelper.DeleteTempFile(@"c:\Diplomarbeit.doc");

Während die Aufrufe Eins und zwei “nur” zu störenden Exceptions führt, wäre Aufruf Nr. Drei sicherlich schon ärgerlicher.

Was tun, lautet also die Frage! “Validierung der Argumente”, oder Einhalten der Regel CA1062 - wie fxCop so schön sagen würde - lautet die Antwort!

Im einfachsten Fall sähe dies wie folgt aus:

  ///<summary>
  /// Deletes the file.
  ///</summary>
  ///<param name="fileName">Please enter valid  (*.tmp)
  /// and existing file names only!</param>
  public static void DeleteTempFile(string fileName)
  {
    if (fileName == null)
    {
      throw new ArgumentNullException("fileName");
    }
    if (System.IO.Path.GetExtension(fileName) != ".tmp")
    {
      throw new ArgumentOutOfRangeException("fileName", "Only temporary (*.tmp) files are allowed!");
    }
    if (!System.IO.File.Exists(fileName))
    {
      throw new ArgumentException("File Not Found", "fileName",
      new System.IO.FileNotFoundException());
    }
    System.IO.File.Delete(fileName);
  }

Vom technischen Standpunkt aus gesehen gibt es an diesem Code nichts auszusetzen. Das Argument fileName wurde ausreichend geprüft und abgesehen von unzureichenden Berechtigungen auf Dateisystemebene dürfte eigentlich nichts mehr schief gehen.

In der Praxis zeigt sich jedoch häufig, dass hier die Bequemheit siegt. Wirft man noch ein Mal einen Blick auf den Quellcode, wird schnell deutlich, dass wir zur Validierung eines Argumentes 13 Zeilen Quellcode geschrieben haben. Die Anzahl der Methoden, die ihre Eingaben nur unzureichend validieren dürfte also klar in der Überzahl sein.

Eleganter und vor allem zeitsparender wäre es doch, wenn ich meinem Code auf dem “kurzen Dienstweg” sagen könnte, welche Anforderungen ich an meine Argumente habe, damit mein Code überhaupt laufen kann. Sahnebonbon wäre es auch, wenn ich zusätzlich noch eine Möglichkeit hätte, dem aufrufenden Code sicher mitzuteilen, ob das was mein Code versucht hat auch wirklich funktioniert hat.

Glücklicherweise gibt es all dies schon. Es hört auf den schönen Namen Design By Contract und hat seinen Ursprung in der Programmiersprache Eiffel. Design By Contract baut auf Vorbedingungen (Preconditions), die erfüllt sein müssen, damit eine Methode laufen kann, und auf Nachbedingungen (Postconditions) auf, die angeben ob meine Methode erfolgreich war. Beides zusammen ergibt den Vertrag (Contract)

Hier gibt es einen sehr schönen Code Project Artikel inklusive einer DBC C# Library (basierend auf .NET 1.x) zu diesem Thema. Übertragen auf unseren Fall sähe dies wie folgt aus:

  public static void DeleteTempFile(string fileName)
  {
    // Postconditions validieren
    Check.Require(fileName != null, "fileName may not be null");
    Check.Require(System.IO.Path.GetExtension(fileName) == ".tmp",
    "Only temporary (*.tmp) files are allowed!");
    Check.Require(System.IO.File.Exists(fileName), "File not found");
    System.IO.File.Delete(fileName);
    // Preconditions validieren
    Check.Ensure(!System.IO.File.Exists(fileName), "File could not be deleted!");
  }

Eigentlich sehr schön, oder? Sobald eine der Prüfungen fehlschlägt wird übrigens eine PreconditionException bzw. PostconditionExceptoin geworfen. Es gibt übrigens noch eine zusätzliche Überladung, die Angabe einer InnerException, wie z. B. ArgumentNullException oder FileNotFoundExceptoin erlaubt.

Noch schöner fände ich es übrigens, wenn die Pre-/Postconditions nicht im Quellcode der Methode stehen, sondern als Attribut annotiert werden würden.

Eine kurze Suche bei Goolge ergab, dass es dazu bereits (mindestens) zwei Ansätze gibt:

Bisher hatte ich leider noch nicht die Zeit, mir eins von beidem anzusehen. Sollte ein Leser dieses Blog-Posts bereits Erfahrung mit einer der beiden Varianten, oder aber einer anderen Attribut-Basierten DBC Implmentierung Erfahrung haben, würde ich mich über einen Kommentar freuen.

… in allen anderen Fällen freue ich mich natürlich auch über Kommentare ;-)

Es gibt 2 Kommentare

Comment by Robert Wachtel
Von | 29.10.2011 21:11
Mit Oxygene (ehemals Chrome) von RemObjects http://www.remobjects.com/ steht übrigens eine in die Visual Studio IDE (2005/2008) integrierte/integrierbare auf Object Pascal basierende Programmiersprache zur Verfügung, die Design by Contract als natives Sprachelement unterstützt.
Comment by Mathias
Von | 29.10.2011 21:11
Ansich ist Spec# eine gute Idee. Ich habe es in meiner Bachelorarbeit verwendet, da habe ich Spec# Code aus einem anderen (visuellen) Design-by-Contract Modell generiert.Leider ist es aber eben nur ein MS Research Projekt. Die Dokumentation ist eher dürftig, es unterstützt wohl noch C# 2.0, und für "echte" Projekte sowohl von der Qualität und wahrscheinlich auch von der Lizenz her nicht geeignet.Mathias