Innovation

Invarianten und Vererbung in Codeverträgen

Dino Esposito

Dino EspositoIn vorangegangenen Ausgaben dieser Rubrik habe ich zwei der gängigsten Softwarevertragstypen erläutert – Vor- und Nachbedingungen – und deren Syntax sowie ihre Semantik aus Perspektive der API für Codeverträge in Microsoft .NET Framework 4 beleuchtet. In dieser Ausgabe stelle ich Ihnen den dritten wichtigen Typ an Softwareverträgen vor – die Invariante – und untersuche anschließend das Verhalten vertragsbasierter Klassen bei angewandter Vererbung.

Invarianten

Allgemein gesagt handelt es sich bei einer Invariante um eine Bedingung, die in einem vorhandenen Kontext immer wahr („true“) ist. Im Zusammenhang mit objektorientierter Software diktiert die Invariante eine Bedingung, die in jeder Instanz einer Klasse immer mit „true“ bewertet wird. Die Invariante ist ein großartiges Tool, das Sie unverzüglich informiert, sobald der Status einer beliebigen Instanz von einer vorhandenen Klasse ungültig wird. Mit anderen Worten: In einem Invariantenvertrag werden formell die Bedingungen festgelegt, unter denen eine Instanz einer Klasse mit einem gültigen Status bewertet wird. So explizit Ihnen das auch erscheinen mag, handelt es sich doch nur um das grundlegende Konzept, das Sie zunächst nachvollziehen und später, wenn Sie ein Geschäft anhand von Klassen modellieren möchten, implementieren müssen. Heute gilt DDD (Domain-Driven Design) als bewährte Methode, um komplexe Geschäftsszenarios zu modellieren. Im Design wird dabei der Invariantenlogik eine wichtige Rolle zugeordnet. Laut DDD wird ausdrücklich empfohlen, niemals mit Instanzen von Klassen zu arbeiten, die sich in einem ungültigen Status befinden. Ebenso wird bei der Nutzung von DDD empfohlen, Factorys der Klassen zu schreiben, damit Objekte mit gültigem Status zurückgegeben werden und damit die Objekte nach jedem Vorgang wieder in einen gültigen Status zurückkehren.

DDD ist eine Methode, bei der Sie die tatsächliche Implementierung der Verträge übernehmen müssen. In .NET Framework 4 werden Sie von Codeverträgen bei der erfolgreichen Implementierung unterstützt, Ihre Arbeit wird dadurch auf ein Minimum reduziert. Im Folgenden erfahren Sie mehr über Invarianten in .NET Framework 4.

Wo ist die Invariantenlogik?

Gibt es einen Zusammenhang zwischen Invarianten und guter Objektmodellierung sowie gutem Softwaredesign? Ein tiefgehendes Verständnis der Domäne ist hier unerlässlich. Die umfassende Kenntnis der Domäne hilft Ihnen, Ihre Invarianten intuitiv aufzuspüren. Und einige Klassen benötigen einfach keine Invarianten. Fehlende Invarianten sind nicht automatisch ein Alarmsignal. Eine Klasse ohne Einschränkung des Inhalts und ihrer Funktion verfügt schlicht nicht über Invariantenbedingungen. Wenn dies das Ergebnis Ihrer Analyse ist, dann ist alles in Ordnung.

Stellen Sie sich eine Klasse vor, mit der Nachrichten veröffentlicht werden sollen. Diese Klasse verfügt wahrscheinlich über einen Titel, eine Kurzbeschreibung und ein Veröffentlichungsdatum. Wo befinden sich hier die Invarianten? Nun, das hängt von der Geschäftsdomäne ab. Ist ein Veröffentlichungsdatum erforderlich? Lautet die Antwort „Ja“, sollten Sie sicherstellen, dass die Nachrichten immer über ein gültiges Datum verfügen. Die Definition eines gültigen Datums wird aus dem Kontext abgeleitet. Ist die Datumsangabe optional, können Sie eine Invariante speichern und gewährleisten, dass der Inhalt der Eigenschaft vor der Verwendung im gegebenen Kontext überprüft wird. Dasselbe gilt für den Titel und die Kurzbeschreibung. Sind Nachrichten ohne Titel und Inhalt sinnvoll? Wenn das für Ihr jeweiliges Geschäftsszenario ratsam erscheint, haben Sie eine Klasse ohne Invarianten. Ansonsten können Sie anhand von Prüfungen sicherstellen, dass Titel und Inhalt nicht leer sind.

Im Allgemeinen gilt, dass eine Klasse ohne Verhalten, die als Container für lose verknüpfte Daten fungiert, wahrscheinlich ohne Invarianten auskommt. Im Zweifelsfall sollten Sie sich bei jeder Eigenschaft der Klasse, unabhängig davon, ob diese durch Methoden als öffentlich, geschützt oder privat festgelegt ist, die Frage stellen: „Kann ich hier einen Wert speichern?“ Damit können Sie definitiv nachvollziehen, ob wichtige Bestandteile des Modells fehlen.

Wie bei vielen anderen Aspekten im Designbereich ist auch hier empfehlenswert, Invarianten zu einem frühen Zeitpunkt im Designprozess zu suchen. Es ist immer möglich, im Entwicklungsprozess eine Invariante zu einem späteren Zeitpunkt hinzuzufügen, in Bezug auf die Umgestaltung ist dies jedoch eine schwierige Aufgabe. In einem solchen Fall sollten Sie vorsichtig sein und sehr umsichtig mit Regression arbeiten.

Invarianten in Codeverträgen

In .NET Framework 4 stellt ein Invariantenvertrag für eine Klasse eine Auflistung von Bedingungen dar, die für jede Instanz der Klasse immer wahr („true“) sein müssen. Wenn Sie Verträge für eine Klasse hinzufügen, gelten die Vorbedingungen für die Fehlersuche beim Klassenaufruf, wohingegen Nachbedingungen und Invarianten für die Fehlersuche in der Klasse und den zugehörigen untergeordneten Klassen verwendet werden.

Der Invariantenvertrag wird anhand von mindestens einer Ad-hoc-Methode definiert. Dabei handelt es sich um private Instanzenmethoden, die nichts zurückgeben und mit einem speziellen Attribut versehen sind: dem Attribut ContractInvariantMethod. Zudem dürfen Invariantenmethoden nur Code für die Aufrufe enthalten, die zum Definieren der Invariantenbedingungen benötigt werden. So können Sie beispielsweise keine Logik – weder reine Logik noch eine andere – in Invariantenmethoden einfügen. Nicht einmal das Hinzufügen einer Logik zum Protokollieren des Klassenstatus ist möglich. So definieren Sie einen Invariantenvertrag für eine Klasse:

public class News {
  public String Title {get; set;}   
  public String Body {get; set;}

  [ContractInvariantMethod]
  private void ObjectInvariant()
  {
    Contract.Invariant(!String.IsNullOrEmpty(Title));
    Contract.Invariant(!String.IsNullOrEmpty(Body));
  }
}

Bei der Klasse für Nachrichten „News“ sind als Invariantenbedingungen festgelegt worden, dass „Title“ (Titel) und „Body“ (Textkorpus) weder den Wert null (0) haben noch leer sein dürfen. Damit dieser Code funktioniert, muss je nach Bedarf in der Projektkonfiguration die vollständige Laufzeitüberprüfung für die verschiedenen Builds aktiviert werden (siehe Abbildung 1).

Invariants Require Full Runtime Checking for Contracts

Abbildung 1 Invarianten benötigen eine vollständige Laufzeitüberprüfung für Verträge

Versuchen Sie es mit folgendem einfachen Codebeispiel:

var n = new News();

Sie werden überrascht sein, dass eine Ausnahme für einen fehlerhaften Vertrag ausgelöst wird. Sie haben die neue Instanz der Klasse „News“ erfolgreich angelegt, aber leider befand sich diese in einem ungültigen Status. Ein neuer Blickwinkel auf Invarianten ist nötig.

In DDD sind Invarianten mit dem Konzept einer Factory verknüpft. Eine Factory ist eine öffentliche Methode, mit der Instanzen einer Klasse erstellt werden. In DDD ist jede Factory dafür zuständig, dass Instanzen von Domänenentitäten mit gültigem Status zurückgegeben werden. Die Quintessenz ist: Bei der Verwendung von Invarianten müssen Sie gewährleisten, dass die Bedingungen jederzeit erfüllt werden.

Aber zu welchem Zeitpunkt genau soll das geschehen? Sowohl DDD als auch die tatsächliche Implementierung der Codeverträge stimmen dahingehend überein, dass die Invarianten jeweils bei Beendigung der einzelnen öffentlichen Methoden geprüft werden sollten, einschließlich Konstruktoren und Setter. In Abbildung 2 wird eine überarbeitete Version der Klasse „News“ angezeigt, in der ein Konstruktor hinzugefügt ist. Eine Factory ist dasselbe wie ein Konstruktor, mit dem Unterschied, dass sie eine statische Methode ist und deshalb einen benutzerdefinierten und kontextsensitiven Namen erhalten kann und zu besser lesbaren Codeabschnitten führt. 

Abbildung 2 Invarianten und invariantenbasierte Konstruktoren

public class News
{
  public News(String title, String body)
  {
    Contract.Requires<ArgumentException>(
      !String.IsNullOrEmpty(title));
    Contract.Requires<ArgumentException>(
      !String.IsNullOrEmpty(body));

    Title = title;
    Body = body;
  }

  public String Title { get; set; }
  public String Body { get; set; }

  [ContractInvariantMethod]
  private void ObjectInvariant()
  {
    Contract.Invariant(!String.IsNullOrEmpty(Title));
    Contract.Invariant(!String.IsNullOrEmpty(Body));
  }
}

Der Code für die Klasse „News“ könnte folgendermaßen lauten:

var n = new News("Title", "This is the news");

Mit diesem Codeabschnitt wird keine Ausnahme ausgelöst, da die Instanz mit einem Status erstellt und zurückgegeben wird, der den Invarianten entspricht. Was passiert, wenn Sie nun folgende Zeile hinzufügen:

var n = new News("Title", "This is the news");
n.Title = "";

Wenn die Eigenschaft „Title“ auf eine leere Zeichenfolge festgelegt wird, erhält das Objekt einen ungültigen Status. Da Invarianten jeweils beim Beenden einer öffentlichen Methode überprüft werden – und Eigenschaften-Setter sind öffentliche Methoden –, wird erneut eine Ausnahme ausgelöst. Interessant dabei ist, dass bei Verwendung öffentlicher Felder anstelle von öffentlichen Eigenschaften keine Überprüfung der Invarianten erfolgt. Der Code wird problemlos ausgeführt. Ihr Objekt befindet sich dennoch in einem ungültigen Status.

Objekte mit einem ungültigen Status sind nicht zwangsläufig ein Problem. In großen Systemen sind Sie jedoch mit einer Verwaltung auf der sicheren Seite, bei der automatisch eine Ausnahme ausgelöst wird, sobald ein ungültiger Status auftritt. Damit können Sie Ihre Entwicklung verwalten und Ihre Tests ausführen. Bei kleineren Anwendungen sind Invarianten häufig nicht erforderlich, auch wenn im Rahmen der Analyse einige auftreten sollten.

Obwohl Invarianten jeweils beim Beenden einer öffentlichen Methode überprüft werden, kann der Status im Textkörper der öffentlichen Methode vorübergehend ungültig sein. Wichtig ist, dass die Invarianten vor und nach der Ausführung von öffentlichen Methoden den Wert „true“ aufweisen.

Wie können Sie verhindern, dass ein Objekt einen ungültigen Status annimmt? Mit einem Tool für die statische Analyse, wie z. B. Microsoft Static Code Checker, lässt sich erkennen, dass eine bestimmte Zuordnung gegen eine Invariante verstoßen wird. Invarianten schützen vor Verhalten, das einen Verstoß nach sich zieht. Sie können aber auch dabei helfen, unzureichende Angaben zu ermitteln. Indem Sie diese ordnungsgemäß spezifizieren, können Sie Fehler im Code, der von einer bestimmten Klasse verwendet wird, einfacher identifizieren.

Vertragsvererbung

In Abbildung 3 wird eine weitere Klasse mit einer definierten Invariantenmethode veranschaulicht. Diese Klasse kann als Stamm für ein Domänenmodell verwendet werden.

Abbildung 3 Invariantenbasierte Stammklasse für ein Domänenmodell

public abstract class DomainObject
{
  public abstract Boolean IsValid();

  [Pure]
  private Boolean IsValidState()
  {
    return IsValid();
  }

  [ContractInvariantMethod]
  private void ObjectInvariant()
  {
    Contract.Invariant(IsValidState());
  }
}

In der Klasse „DomainObject“ wird die Invariantenbedingung anhand einer privaten Methode ausgedrückt, die als reine Methode deklariert ist (das heißt, ihr Status ändert sich nicht). Intern wird von der privaten Methode eine abstrakte Methode aufgerufen, die von abgeleiteten Klassen zur Angabe ihrer eigenen Invarianten verwendet wird. In Abbildung 4 wird eine aus „DomainObject“ abgeleitete, mögliche Klasse dargestellt, von der die Methode „IsValid“ außer Kraft gesetzt wird.

Abbildung 4 Überschreiben einer von Invarianten verwendeten Methode

public class Customer : DomainObject
{
  private Int32 _id;
  private String _companyName, _contact;

  public Customer(Int32 id, String company)
  {
    Contract.Requires(id > 0);
    Contract.Requires(company.Length > 5);
    Contract.Requires(!String.IsNullOrWhiteSpace(company));

    Id = id;
    CompanyName = company;
  }
  ...
  public override bool IsValid()
  {
    return (Id > 0 && !String.IsNullOrWhiteSpace(CompanyName)); 
  }
}

Die Lösung scheint elegant und effektiv zu sein. Nun versuchen wir, mit einer neuen Instanz der Klasse „Customer“ gültige Daten zu übergeben:

var c = new Customer(1, "DinoEs");

Wenn wir den Konstruktor „Customer“ nicht beachten, sieht alles gut aus. Da jedoch „Customer“ von „DomainObject“ erbt, wird der Konstruktor „DomainObject“ aufgerufen, und die Invariante wird überprüft. Aufgrund der Tatsache, dass „IsValid“ von „DomainObject“ virtuell (tatsächlich abstrakt) ist, wird der Aufruf an „IsValid“ definiert für „Customer“ weitergeleitet. Unpraktischerweise erfolgt die Prüfung so für eine Instanz, die nicht gänzlich initialisiert wurde. Eine Ausnahme wird ausgelöst, was aber nicht Ihr Fehler ist. (In der neuesten Version der Codeverträge ist dieses Problem behoben. Die Invariantenprüfung bei Konstruktoren wird verzögert, bis der Aufruf des äußersten Konstruktors erfolgt ist.)

Dieses Szenario passt zu einem bekannten Problem: Rufen Sie keine virtuellen Mitglieder von einem Konstruktor auf. In diesem Fall geschieht das nicht aufgrund der direkten Codeabschnitte, sondern als Nebenwirkung der Vertragsvererbung. Ihnen stehen zwei Lösungen zur Verfügung: Sie können das abstrakte IsValid-Element aus das Basisklasse verwerfen, oder Sie greifen auf das in Abbildung 5 veranschaulichte Codebeispiel zurück.

Abbildung 5 Überschreiben einer von Invarianten verwendeten Methode

public abstract class DomainObject
{
  protected Boolean Initialized; 
  public abstract Boolean IsValid();

  [Pure]
  private Boolean IsInValidState()
  {
    return !Initialized || IsValid();
  }

  [ContractInvariantMethod]
  private void ObjectInvariant()
  {
    Contract.Invariant(IsInValidState());
  }
}

public class Customer : DomainObject
{
  public Customer(Int32 id, String company)
  {
     ...
    Id = id;
    CompanyName = company;
    Initialized = true;
  }
     ...
}

Das geschützte Mitglied „Initialized“ fungiert als Schutz, sodass die außer Kraft gesetzte IsValid-Methode erst aufgerufen wird, nachdem das tatsächliche Objekt initialisiert worden ist. Bei dem Mitglied „Initialized“ kann es sich um ein Feld oder eine Eigenschaft handeln. Ist es eine Eigenschaft, werden die Invarianten ein zweites Mal durchlaufen. Das ist nicht direkt erforderlich, da alles ja bereits einmal überprüft wurde. Demzufolge wird bei Nutzung eines Felds der Code ein kleines bisschen schneller ausgeführt.

Die Vertragsvererbung erfolgt dahingehend automatisch, dass eine abgeleitete Klasse automatisch die für ihre Basisklasse festgelegten Verträge erhält. Auf diese Weise werden die Vorbedingungen der abgeleiteten Klasse zu den Vorbedingungen der Basisklasse hinzugefügt. Dasselbe gilt für Nachbedingungen und Invarianten. Im Rahmen einer Vererbungskette werden die Verträge von einem Intermediate Language Rewriter zusammengefasst und bei Bedarf in der korrekten Reihenfolge aufgerufen.

Seien Sie vorsichtig

Invarianten sind nicht unfehlbar. Manchmal können Invarianten auf eine Art hilfreich sein, aber auf andere Weise Probleme verursachen. Dies gilt besonders, wenn sie in allen Klassen und im Kontext einer Klassenhierarchie eingesetzt werden. Sie sollten immer bemüht sein, die Invariantenlogik in Ihren Klassen zu erkennen. Falls Sie beim Implementieren von Invarianten allerdings auf einen Grenzfall stoßen, würde ich sagen, dass Sie die Invarianten am besten aus der Implementierung ausklammern. Beachten Sie jedoch, dass solche Grenzfälle meist auf die Komplexität im Modell zurückzuführen sind. Invarianten sind keine Probleme, sondern Tools, mit denen Sie Implementierungsprobleme beheben können.

Es gibt mindestens einen Grund, warum das Zusammenfassen von Verträgen in einer Klassenhierarchie ein schwieriges Vorhaben ist. Diese Problematik sprengt den heutigen Rahmen, wird aber in einem Artikel in der nächsten Ausgabe ausführlich besprochen.

Dino Esposito ist der Verfasser von „Programming Microsoft ASP.NET 4“ (Microsoft Press, 2011) und Mitverfasser von „Microsoft .NET: Architecting Applications for the Enterprise“ (Microsoft Press 2008). Esposito lebt in Italien und ist ein weltweit gefragter Referent für Branchenveranstaltungen. Sie können ihn auf Twitter unter twitter.com/despos erreichen.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Manuel Fahndrich und Brian Grunkemeyer