Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

 << zurück
Praxisbuch Objektorientierung von Bernhard Lahres, Gregor Raýman
Professionelle Entwurfsverfahren
Buch: Praxisbuch Objektorientierung

Praxisbuch Objektorientierung
609 S., 49,90 Euro
Rheinwerk Computing
ISBN 3-89842-624-6
gp Kapitel 7 Abläufe in einem objektorientierten System
  gp 7.1 Erzeugung von Objekten mit Konstruktoren und Prototypen
    gp 7.1.1 Konstruktoren: Klassen als Vorlagen für ihre Exemplare
    gp 7.1.2 Prototypen als Vorlagen für Objekte
    gp 7.1.3 Entwurfsmuster Prototyp
  gp 7.2 Fabriken als Abstraktionsebene für die Objekterzeugung
    gp 7.2.1 Statische Fabriken
    gp 7.2.2 Abstrakte Fabriken
    gp 7.2.3 Konfigurierbare Fabriken
    gp 7.2.4 Registraturen für Objekte
    gp 7.2.5 Fabrikmethoden
    gp 7.2.6 Erzeugung von Objekten als Singletons
    gp 7.2.7 Dependency Injection
  gp 7.3 Objekte löschen
    gp 7.3.1 Speicherbereiche für Objekte
    gp 7.3.2 Was ist eine Garbage Collection?
    gp 7.3.3 Umsetzung einer Garbage Collection
  gp 7.4 Objekte in Aktion und in Interaktion
    gp 7.4.1 UML: Diagramme zur Beschreibung von Abläufen
    gp 7.4.2 Nachrichten an Objekte
    gp 7.4.3 Iteratoren und Generatoren
    gp 7.4.4 Funktionsobjekte und ihr Einsatz als Eventhandler
    gp 7.4.5 Kopien von Objekten
    gp 7.4.6 Sortierung von Objekten
  gp 7.5 Kontrakte: Objekte als Vertragspartner
    gp 7.5.1 Überprüfung von Kontrakten
    gp 7.5.2 Übernahme von Verantwortung: Unterklassen
in der Pflicht
    gp 7.5.3 Prüfungen von Kontrakten bei Entwicklung
und Betrieb
  gp 7.6 Exceptions: Wenn der Kontrakt nicht
eingehalten werden kann
    gp 7.6.1 Exceptions in der Übersicht
    gp 7.6.2 Exceptions und der Kontrollfluss eines Programms
    gp 7.6.3 Exceptions im Einsatz bei Kontraktverletzungen
    gp 7.6.4 Exceptions als Teil eines Kontraktes
    gp 7.6.5 Der Umgang mit Checked Exceptions
    gp 7.6.6 Exceptions in der Zusammenfassung


Rheinwerk Computing

7.5 Kontrakte: Objekte als Vertragspartner  downtop

Ein Objekt stellt in der Regel eine Reihe von Operationen zur Verfügung, die auf ihm ausgeführt werden können.

Dabei wird die Syntax der Operation durch die Regeln einer Programmiersprache sehr genau beschrieben: Name der Operation, Zahl und Art der zu übergebenden Parameter, genaue Schreibweise des Aufrufs, all das wird exakt festgelegt.

Semantik der Operation

Wie steht es aber mit dem Teil, den wir als Semantik der Operation bezeichnen? Wo wird festgelegt, was die Umsetzung der Operation (die entsprechende Methode des Objekts) denn genau leisten soll? Wie wir durch das Prinzip der Trennung der Schnittstelle von der Implementierung festgelegt haben, soll ein Nutzer eben nicht die Implementierung betrachten, um herauszufinden, was eine Methode leistet. In diesem Abschnitt werden wir darauf eingehen, wie Kontrakte für Klassen überprüft werden können. Außerdem werden wir an konkreten Beispielen vorstellen, wie ausformulierte Kontrakte dabei helfen können, Fehler im Design zu erkennen.


Rheinwerk Computing

7.5.1 Überprüfung von Kontrakten  downtop

In Abschnitt 4.2.2, Kontrakte: Die Spezifikation einer Klasse, haben wir vorgestellt, wie formale Kontrakte zur Spezifikation einer Klasse verwendet werden können. Dabei kamen Vorbedingungen, Nachbedingungen und Invarianten zum Einsatz, und wir haben die OCL-Notation dafür vorgestellt. Wir wiederholen die Abbildung des OCL-Beispiels in Abbildung 7.47, da die OCL-Notation in den folgenden Abschnitten häufiger zum Einsatz kommt.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 7.47   Beispiel für OCL-Notation: Vor- und Nachbedingung

Im abgebildeten Beispiel sind die Vor- und Nachbedingungen für die Operation kuendigen der Klasse ZeitungsAbo beschrieben. In diesem Abschnitt gehen wir nun darauf ein, wie diese Bedingungen zur Laufzeit eines Programms geprüft werden können.

Nur wenige Programmiersprachen erlauben eine direkte Übertragung von Vorbedingungen, Nachbedingungen und Invarianten in die Sprache und stellen integrierte Konstrukte dafür zur Verfügung. Die Sprache Eiffel bietet dafür die Sprachelemente require, ensure und invariant. In anderen Sprachen hat sich der Mechanismus der so genannten Zusicherungen (Assertions) eingebürgert.

Abbildung


Zusicherungen (Assertions)

Zusicherungen sind Ausdrücke, die entweder wahr oder falsch sind und an definierten Stellen im Ablauf eines Programms ausgewertet werden. Zusicherungen sollen sicherstellen, dass der Zustand des Programms zum Zeitpunkt der Auswertung korrekt ist. Zusicherungen werden häufig benutzt, um Vorbedingungen und Nachbedingungen von Operationen abzubilden.


Zusicherungen differenzieren nicht mehr, ob es sich um die Prüfung einer Vorbedingung, einer Nachbedingung, einer Invariante oder vielleicht einfach nur eines Zwischenzustands handelt. Je nach der Stelle im Quellcode, an der sie auftauchen, können sie alle genannten Rollen übernehmen.

Eine Umsetzung der in OCL dargestellten Vor- und Nachbedingungen in Java kann zum Beispiel aussehen wie in Listing 7.41.

class ZeitungsAbo {
          ...
          void kuendigen(Date kuendigungsDatum) {
             assert(status != AboStatus.gekuendigt);
             assert(kuendigungsDatum >= this.fruehesteKuendigung);
             ...
             assert(status == AboStatus.gekuendigt);
          }
          ...

Listing 7.41   Prüfung von Vor- und Nachbedingungen in Java

Mit OCL und der programmiersprachlichen Konstrukten zur Absicherung von Bedingungen stehen die technischen Möglichkeiten zur Verfügung, mit denen Sie Kontrakte zwischen Modulen beschreiben können. Im folgenden Abschnitt werden Sie erfahren, wie Kontrakte gerade mit Blick auf das wichtige Prinzip der Ersetzbarkeit formuliert werden können. Sie werden dabei sehen, dass die Ausformulierung von Kontrakten dazu führen kann, dass Fehler im Klassenentwurf schneller erkannt werden.


Rheinwerk Computing

7.5.2 Übernahme von Verantwortung: Unterklassen in der Pflicht  downtop

Bereits in Abschnitt 5.1.3 haben Sie das Prinzip der Ersetzbarkeit kennen gelernt. Dieses fordert, dass ein Exemplar einer Unterklasse an jeder Stelle anstatt eines Exemplars der Oberklasse eingesetzt werden kann. Dabei wurden auch drei Konsequenzen des Prinzips der Ersetzbarkeit für Unterklassen aufgeführt:

gp  Unterklassen dürfen die Vorbedingungen für Operationen nicht verschärfen.
gp  Unterklassen dürfen die Nachbedingungen einer Operation nicht einschränken.
gp  Unterklassen müssen sicherstellen, dass die Invarianten der Oberklasse eingehalten werden.

Lassen Sie uns im Folgenden zwei Beispiele betrachten, eines, bei dem das Prinzip der Ersetzbarkeit in Bezug auf Vor- und Nachbedingungen erfüllt ist, und anschließend eines, bei dem das Prinzip eklatant verletzt wird. Anschließend werden Sie ebenfalls anhand eines Beispiels sehen, an welchen Stellen im Code eine Überprüfung von Kontrakten sinnvoll ist.

Ein korrektes Beispiel: Das Prinzip der Ersetzbarkeit wird eingehalten

Was bedeuten die Anforderungen an Vor- und Nachbedingungen denn nun in der Praxis? Betrachten Sie dazu als Beispiel die in Abbildung 7.48 dargestellte einfache Hierarchie von Klassen, die sich auf Bankkonten beziehen.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 7.48   Hierarchie von Bankkonten

Sie sehen eine Klasse BankKonto als Basisklasse, von der die Klasse KreditKonto abgeleitet ist. Beide Klassen spezifizieren eine Operation abheben, die laut Beschreibung den Kontostand um den dabei angegebenen Betrag vermindert. Dabei überschreibt die Klasse KreditKonto die bereits in der Klasse BankKonto umgesetzte Methode für das Abheben. Aus der Darstellung geht aber noch nicht hervor, worin sich die beiden Umsetzungen denn nun unterscheiden. Deshalb sind in Abbildung 7.49 die jeweiligen Vor- und Nachbedingungen der Operation unter Verwendung der OCL dargestellt.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 7.49   Vor- und Nachbedingungen der Operation abheben

Für ein generelles Bankkonto ist es die Vorbedingung für das Abheben, dass genug Geld auf dem Konto ist. Sie können höchstens so viel abheben, wie auf dem Konto ist. Dies wird durch die Vorbedingung pre: betrag <= kontostand ausgedrückt. Die Nachbedingung ist, dass der neue Kontostand um den angegebenen Betrag reduziert wurde: post: kontostand = kontostand@prebetrag.

Die Klasse KreditKonto ändert die Nachbedingung für die Operation abheben gegenüber der Klasse BankKonto nicht. Damit ist die zweite Konsequenz des Prinzips der Ersetzbarkeit eingehalten: Die Nachbedingungen dürfen nicht gelockert werden. Wie sieht es aber mit der Vorbedingung aus?

Bei einem Kreditkonto ist eine Abhebung auch dann möglich, wenn der Kontostand negativ ist, sofern ein vorgegebenes Kreditlimit nicht überschritten wird. Die Vorbedingung lautet nun als pre: betrag <= (kontostand + kreditlimit). Da das Kreditlimit nicht negativ sein kann, ist diese Vorbedingung weniger restriktiv als die Vorbedingung in der Klasse BankKonto: Eine Abhebung ist immer noch möglich, wenn der Kontostand den abzuhebenden Betrag noch deckt. Zusätzlich kann aber auch eine Abhebung stattfinden, wenn zwar das Konto den Betrag nicht mehr hergibt, der gewährte Kreditrahmen aber ausreichend ist.

Damit ist auch die erste Konsequenz der Prinzips der Ersetzbarkeit eingehalten: Unterklassen dürfen die Vorbedingungen von Operationen nicht verschärfen. In Listing 7.42 ist die Umsetzung der Klasse BankKonto in Java aufgeführt. In der Methode abheben wird die Vorbedingung durch eine Zusicherung abgeprüft. Die Nachbedingung wird im Code nicht geprüft.

class BankKonto {
          int kontostand;
      
          BankKonto(int kontostand) {
              this.kontostand = kontostand;
          }
          int kontostand(){
              return kontostand;
          }
          void abheben(int betrag) {
              assert(betrag <= kontostand);
              kontostand -= betrag;
          }
          void einzahlen(int betrag) {
              kontostand += betrag;
          }
      }

Listing 7.42   Umsetzung der Klasse BankKonto

In Listing 7.43 ist die Umsetzung der Klasse KreditKonto zu sehen. Dort wird die (gelockerte) Vorbedingung ebenfalls in der Methode abheben geprüft.

class KreditKonto extends BankKonto {
          int kreditlimit;
          ...
          void abheben(int betrag) {
              assert(betrag <= (kontostand + kreditlimit));
              kontostand -= betrag;
          }
      }

Listing 7.43   Umsetzung der Klasse KreditKonto

Prinzip der Ersetzbarkeit eingehalten

Unsere Klassenhierarchie und ihre Umsetzung erfüllt das Prinzip der Ersetzbarkeit, da die Klasse KreditKonto den Kontrakt der Klasse BankKonto immer noch einhält. Wenn Geld auf dem Konto ist, kann eine Abhebung erfolgen. Um die Einhaltung der Vorbedingungen zu prüfen, haben wir zwei Zusicherungen in den beiden Methoden eingefügt. Dadurch wird von beiden Klassen der jeweilige Kontrakt korrekt überprüft.

Eine Verletzung des Prinzips der Ersetzbarkeit ist allerdings nicht immer offensichtlich. Im Folgenden werden Sie deshalb ein sehr ähnlich aussehendes Beispiel kennen lernen, bei dem das Prinzip trotzdem verletzt wird.

Ein fehlerhaftes Beispiel: Das Prinzip der Ersetzbarkeit wird verletzt

Eine Verletzung des Prinzips der Ersetzbarkeit erkennen Sie daran, dass für eine Operation in einer abgeleiteten Klasse entweder die Vorbedingungen verschärft oder die Nachbedingungen gelockert werden. Die abgeleitete Klasse hält also den Kontrakt der Basisklasse nicht mehr ein.

Für unser Beispiel wählen wir wieder eine Klasse BankKonto mit den Basisoperationen einzahlen und abheben. Zusätzlich fügen wir aber die Operationen ueberweisen und modifiziereKreditLimit hinzu.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 7.50   Eine Klasse SparKonto ist von BankKonto abgeleitet.

Die beiden zusätzlichen Operationen machen auf den ersten Blick so durchaus Sinn. In Abbildung 7.50 sehen Sie auch eine Klasse SparKonto, die von BankKonto abgeleitet ist. Der Klasse SparKonto sind dabei ebenfalls Methoden für die Operationen ueberweisen und modifiziereKreditLimit zugeordnet.

In Abbildung 7.51 sehen Sie die Vor- und Nachbedingungen für die Operation überweisen aufgelistet.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 7.51   Vorbedingungen für die Operation überweisen

Die Vorbedingung für eine Überweisung, wie sie für die Klasse BankKonto formuliert ist, macht durchaus Sinn: Es muss genügend Geld auf dem Konto sein, so dass das Kreditlimit nicht überschritten wird. Die Bedingung lautet also pre: betrag <= (kontostand + kreditlimit).

Die Umsetzung der Operation in der Klasse BankKonto sichert dann auch genau das zu.

class BankKonto
          ...
          void ueberweisen(int betrag, BankKonto zielkonto)
          {
              assert(betrag <= (kontostand + kreditlimit));
              abheben(betrag);
              zielkonto.einzahlen(betrag);
          }

Nun stellen wir aber fest, dass eine Überweisung von einem Sparkonto gar nicht möglich ist. Bei Sparkonten kann nur eingezahlt und abgehoben werden. Damit ist die Operation ueberweisen auf einem Sparkonto nicht zulässig. Eine mögliche Konsequenz ist der unten stehende Code.

class SparKonto extends BankKonto {
          ...
          // Überweisungen von einem Sparkonto nicht möglich
          @Override
          void ueberweisen(int betrag, BankKonto zielkonto) {
              assert(false);
          }

assert(false) in Methoden

Die Vorbedingung für den Aufruf der Operation wird radikal eingeschränkt, es ist nun nämlich überhaupt nicht mehr zulässig, die Operation auf einem Exemplar von SparKonto aufzurufen. In der OCL-Darstellung von Abbildung 7.51 wird das dadurch deutlich, dass die Bedingung nun pre: false lautet.

Das ist ein ganz klarer Indikator dafür, dass das Prinzip der Ersetzbarkeit für diesen Fall nicht gilt. Die vorgestellte Modellierung verletzt damit das Prinzip der Ersetzbarkeit.

Aber die beschriebene Modellierung ist nicht nur unter diesem Aspekt fehlerhaft. Auch die Anpassung eines Kreditlimits macht für ein Sparkonto keinen Sinn. Wenn Sie schon einmal versucht haben, Ihr Sparbuch zu überziehen, werden Sie das bemerkt haben. Die Klasse SparKonto muss mit diesem Konflikt umgehen. In Abbildung 7.52 sind die Vor- und Nachbedingungen für die beiden beteiligten Klassen bezüglich der Operation modifiziereKreditLimit dargestellt.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 7.52   Radikale Lockerung der Nachbedingung

Die Klasse BankKonto verlangt als Vorbedingung, dass das neue Kreditlimit nicht negativ sein darf. Als Nachbedingung verspricht sie, dass das Limit entsprechend dem Betrag angepasst wird.

class BankKonto {
          ...
          void modifiziereKreditLimit(int betrag) {
              assert((kreditlimit + betrag) >= 0);
              int kreditlimit_pre = kreditlimit;
              kreditlimit += betrag;
              assert(kreditlimit == kreditlimit_pre + betrag);
          }
          ...

Bei einem SparKonto gibt es kein Kreditlimit. Eine Möglichkeit ist es also, die Operation in dieser Klasse so umzusetzen, dass sie einfach nichts tut.

    // Limiterhöhung hat bei Sparkonten keine Auswirkung
          @Override
          void modifiziereKreditLimit(int betrag) {
          }

Dadurch wird auf der einen Seite die Vorbedingung gelockert, was zulässig ist. Es gibt nun nämlich gar keine Einschränkung in den Vorbedingungen mehr. In Abbildung 7.52 ist das daran erkennbar, dass die Vorbedingung durch pre: true beschrieben wird.

Auf der anderen Seite hält die Methode aber die Versprechung der Nachbedingung nicht mehr ein, die für die Klasse BankKonto ebenfalls aus der Abbildung als post: kreditlimit = kreditlimit@pre + betrag zu entnehmen ist. Die neue Nachbedingung in der Umsetzung durch die Klasse SparKonto lautet nämlich post: true. Damit wird überhaupt keine Zusicherung mehr gemacht, die Nachbedingung ist radikal gelockert worden.

Methoden leer überschrieben

Hier sehen Sie einen weiteren Indikator für die Verletzung des Prinzips der Ersetzbarkeit: Bereits implementierte Methoden werden in abgeleiteten Klassen leer überschrieben. Auch aus diesem Grund verletzt die vorgestellte Modellierung das Prinzip der Ersetzbarkeit.

Wer prüft Kontrakte: Aufrufer oder Aufgerufener?

Wir haben in den bisherigen Beispielen die Überprüfung der Kontrakte mit ihren Vor- und Nachbedingungen in die Verantwortung des Objekts gelegt, dessen Methode aufgerufen wird. Das ist die im Allgemeinen verwendete Variante. Auf den ersten Blick ist dies auch das bessere Vorgehen. Wenn Sie die Zusicherung vor jedem Aufruf überprüfen müssten, wären diese Prüfungen weit über den Code verstreut und damit nur mit großem Aufwand änderbar.

Aber prüfen Sie mit diesem Vorgehen überhaupt die Einhaltung des Kontrakts? Kontrakte beziehen sich nicht auf Implementierungen, sondern auf Schnittstellen. Betrachten Sie zur Illustration ein Beispiel aus einer etwas anderen Domäne.

Öko-Tankstelle mit Salatöl

Nehmen Sie einfach einmal an, Sie sind Betreiber der Öko-Tankstelle in Abbildung 7.53 und vertreiben Salatöl als Treibstoff. Dieses Salatöl kann von speziell umgerüsteten Dieselfahrzeugen verwendet werden, die aber nach wie vor alternativ auch mit Diesel fahren können. Normale Dieselfahrzeuge dürfen damit aber nicht betankt werden, da sich der Motor sonst stinkend und rauchend selbst zerstören würde.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 7.53   Eine Tankstelle für umgerüstete Dieselfahrzeuge

In Abbildung 7.54 sehen Sie eine mögliche Modellierung solcher Fahrzeuge und einer zugehörigen Tankstelle.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 7.54   Herkömmliche Dieselautos und umgerüstete Dieselautos

Sie setzen also die Methode tanken für beide beteiligte Klassen um: Umgerüstete Dieselautos sind in unserem Modell eine Spezialisierung von normalen Dieselautos. Mit dem Rüstzeug aus den vorhergehenden Abschnitten statten Sie die Operation tanken aber auch gleich mit den entsprechenden Vorbedingungen aus, um den Kontrakt der Operation explizit zu formulieren. In Abbildung 7.55 sind die Vorbedingungen für die Umsetzung in beiden Klassen aufgeführt. Für ein DieselAuto gilt die Vorbedingung, dass der verwendete Kraftstoff vom Typ Diesel sein muss. Für ein UmgerüstetesDieselAuto kann es aber auch Salatöl sein.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 7.55   Vorbedingungen für Operation tanken

Prinzip der Ersetzbarkeit erfüllt

Zunächst können Sie daraus entnehmen, dass die Modellierung das Prinzip der Ersetzbarkeit erfüllt. Die Unterklasse lockert die Vorbedingung, indem sie Diesel als Kraftstoff immer noch zulässt, aber auch Salatöl akzeptiert.

class DieselAuto {
      
          void tanken(Kraftstoff kraftstoff)
          {
              assert(kraftstoff.typ() == KraftstoffTyp.Diesel);
              System.out.println("Dieselauto " +
                "wird betankt mit " + kraftstoff.toString());
          }
      }
      
      class UmgerüstetesDieselAuto extends DieselAuto {
          void tanken(Kraftstoff kraftstoff)
          {
              assert(kraftstoff.typ() == KraftstoffTyp.Diesel
                   || kraftstoff.typ() == KraftstoffTyp.SalatOel);
              System.out.println("Umgerüstetes Dieselauto " +
                 "wird betankt mit " + kraftstoff.toString());
          }
      }

Listing 7.44   Überprüfung der Vorbedingungen für die Operation tanken

In Listing 7.44 ist die Umsetzung der Prüfungen im Java-Source-Code aufgeführt.

Die Tankstelle haben Sie in unserem Szenario von einem weniger ökologisch orientierten Vorbesitzer übernommen. Deshalb ist die Operation der Tankstelle, mit der die Autos betankt werden, generell für Dieselautos ausgelegt.

class Tankstelle {
          private Kraftstoff kraftstoff;
          void oeffnen() {
              this.kraftstoff =
                  new Kraftstoff(KraftstoffTyp.SalatOel);
          }
          void betanken(DieselAuto auto)
          {
              auto.tanken(this.kraftstoff);
          }
      }

Sie machen vor der Eröffnung Ihrer Tankstelle ein größere Zahl von Testläufen, die mit Salatöl betriebenen Autos Ihrer Freunde rollen alle an. Alles läuft prächtig.

Tankstelle tankstelle = new Tankstelle();
      tankstelle.oeffnen();
      UmgerüstetesDieselAuto pkw1 = new UmgerüstetesDieselAuto();
      UmgerüstetesDieselAuto pkw2 = new UmgerüstetesDieselAuto();
      tankstelle.betanken(pkw1);
      tankstelle.betanken(pkw2);

Sie erhalten die unten stehende Ausgabe.

Umgerüstetes Dieselauto wird betankt mit SalatOel
      Umgerüstetes Dieselauto wird betankt mit SalatOel

Verletzung des Kontrakts

Sie stellen offensichtlich keine Verletzung unseres Kontrakts fest, da immer die Methode tanken der spezialisierten Klasse UmgerüstetesDieselAuto aufgerufen wird. Aber erinnern Sie sich: Die Vorbedingungen der Operation tanken sehen für die Klasse DieselAuto ganz anders aus als für die Klasse UmgerüstetesDieselAuto. Die umgerüsteten Autos sind wesentlich toleranter. An der Aufrufstelle der Operation tanken kann aber jedes beliebige Dieselauto vorbeikommen.

Deshalb tickt hier eine Zeitbombe, denn faktisch liegt beim Aufruf der Operation tanken eine mögliche Kontraktverletzung vor.

Diese mögliche Verletzung des Kontrakts haben Sie aber nicht erkannt, weil Sie die Kontrolle über die Einhaltung der Vorbedingungen in den konkreten Methoden vornehmen. Dort ist die Verletzung nicht mehr erkennbar. Bisher ging alles gut, aber nur weil noch kein echtes Dieselfahrzeug Ihre Tankstelle angesteuert hat.

Als nun ein paar Tage später ein fetter LKW an Ihre Zapfsäule rollt, ist dieser natürlich nicht auf Salatöl vorbereitet.

DieselAuto lkw = new DieselAuto();
      tankstelle.betanken(lkw);

Die Ausgabe sieht nun weniger freundlich aus.

Exception in thread "main" java.lang.AssertionError
      at DieselAuto.tanken(TankstellenTest.java:33)
      at Tankstelle.betanken(TankstellenTest.java:24)
      at TankstellenTest.main(TankstellenTest.java:13)

Testläufe finden Verletzung nicht.

Sie haben die Kontraktverletzung bei den ganzen Testläufen nicht bemerkt, und nun steht erst einmal der Betrieb Ihrer Tankstelle, während Sie einem aufgebrachten LKW-Fahrer erklären dürfen, warum er hier keinen Kraftstoff erhalten wird.

Aber warum eigentlich haben Sie die Kontraktverletzung bei den Tests nicht bemerkt? Sie haben doch alle Regeln befolgt und die Prüfung der Kontrakte in den beiden Methoden verankert, welche die Operation tanken jeweils umsetzen.

Nun, das Problem liegt darin, dass die Prüfung des Kontrakts in den realisierenden Methoden vorgenommen wurde. Damit erfolgte die Prüfung eben nicht gegenüber der abstrakten Schnittstelle, sondern gegenüber der Implementierung. Nur wenn diese Implementierung durchlaufen wird, kann die Verletzung des Kontrakts auch festgestellt werden.

Prüfung von Kontrakten


Prüfung von Kontrakten beim Aufruf von Operationen

Kontrakte bezüglich Vorbedingungen sollen mit den Informationen geprüft werden, die beim Aufruf der Operation zur Verfügung stehen. Damit findet eine Überprüfung gegenüber der Schnittstelle statt. Wird eine Vorbedingung erst bei der Umsetzung einer Operation überprüft, ist die Prüfung lückenhaft und hängt davon ab, welche Implementierung für das Ausführen der Operation verwendet wird. Durch eine Prüfung an der Aufrufstelle werden nicht nur faktische, sondern auch mögliche Kontraktverletzungen bezüglich der Vorbedingungen gefunden.


Sie werden gleich sehen, dass diese Forderung alleine mit den Mitteln der Objektorientierung nur schwer zu erfüllen ist und aspektorientierte Erweiterungen notwendig sind, um sie praktikabel umzusetzen.

Zunächst wollen wir jedoch erläutern, warum diese Forderung sehr sinnvoll ist. Betrachten Sie dazu das Beispiel der Salatöl-Tankstelle in etwas angepasster Form. Die obige Forderung verlangt von uns, dass die Einhaltung des Kontrakts an der Aufrufstelle der Operation tanken überprüft werden soll.

In Abbildung 7.56 ist der Ablauf beim Betanken in der Übersicht dargestellt. Dabei sind die beiden möglichen Stellen für die Prüfung des Kontrakts markiert.

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 7.56   Ablauf beim Betanken eines umgerüsteten Dieselautos

Wenn Sie die Variante der Überprüfung an der Aufrufstelle wählen, resultiert der folgende Source-Code.

    void betanken(DieselAuto auto)
          {
              assert(this.kraftstoff.typ() ==
                            KraftstoffTyp.Diesel);
              auto.tanken(this.kraftstoff);
          }

Bei dieser Variante hätten die durchgeführten Testläufe ergeben, dass eine nicht vertragsgemäße Nutzung der Operation tanken vorliegt. Denn nun würde auch das Betanken eines umgerüsteten Autos dazu führen, dass die Kontraktverletzung bereits hier erkannt wird. Da wir nämlich an dieser Stelle nur die allgemeinere Information zur Verfügung haben, dass wir ein DieselAuto (und nicht unbedingt ein umgerüstetes) vorliegen haben, muss auch der Kontrakt, den wir mit der Klasse DieselAuto haben, geprüft werden. Und der ist restriktiver in Bezug auf die Vorbedingungen als der Kontrakt mit der Klasse der umgerüsteten Autos. Mit einer Prüfung an der Aufrufstelle hätten Sie also schon beim ersten Testlauf festgestellt, dass Ihr Programm fehlerhaft ist und korrigiert werden muss.

Prüfung des Kontrakts an Aufrufstelle

Warum also nicht grundsätzlich die Prüfung des Kontrakts an die Stelle verlagern, an der eine Operation aufgerufen wird? Im obigen Beispiel haben wir doch gesehen, dass diese Variante erst wirklich korrekt auf die Einhaltung eines Kontrakts prüft.

Leider hat diese Lösung in der Praxis einen Haken, und in den meisten Fällen werden Sie die Prüfung aus einem ganz pragmatischen Grund nicht an die Aufrufstelle verlagern können: Es gibt in der Regel wesentlich mehr Aufrufstellen für eine Operation, als es Implementierungen davon gibt. Sie müssten die Prüfungen also redundant über Code verteilen, den Sie möglicherweise selbst gar nicht kennen. Damit wird die Wartung dieser Prüfungen schnell zu einem Albtraum. Über einige der Aufrufstellen haben Sie möglicherweise gar keine Kontrolle, da sie sich in anderen Modulen befinden oder von anderen Teams oder Firmen entwickelt werden. Obwohl Sie dadurch die Prüfung der Kontrakte korrekt gestalten können, ist diese Lösung mit den herkömmlichen Mitteln der Objektorientierung nicht praktikabel umsetzbar.

Eine elegante Lösung für diesen Konflikt bieten die Techniken der Aspektorientierung.

Aspekte als Lösung


Aspektorientierte Erweiterungen zur Prüfung von Kontrakten

Aspektorientierte Frameworks und Spracherweiterungen ermöglichen es, die Prüfung von Kontrakten beim Aufruf von Operationen vorzunehmen, ohne dass diese Prüfungen über den Code verteilt werden müssen.

Mit den herkömmlichen Methoden der Objektorientierung ist eine solche Prüfung nicht möglich, ohne die Struktur des Codes in Bezug auf die Überprüfungen von Kontrakten sehr unübersichtlich zu machen.


In Abschnitt 9.3.4, Design by Contract, finden Sie ein Beispiel, wie die Prüfungen mit Mitteln der Aspektorientierung umgesetzt werden können.


Rheinwerk Computing

7.5.3 Prüfungen von Kontrakten bei Entwicklung und Betrieb  toptop

Pacta sunt servanda (Verträge müssen gehalten werden) ist ein Grundsatz des privaten und öffentlichen Rechts. Aber auch im juristischen Bereich muss immer eine Einschätzung getroffen werden, mit welchen Mitteln die Einhaltung von Kontrakten geprüft wird. Ähnliche Abwägungen müssen Sie auch bei Kontrakten zwischen verschiedenen Klassen oder Modulen treffen.

Sie haben im vorigen Abschnitt mehrere Möglichkeiten gesehen, Kontrakte explizit beim Ablauf eines Programms zu überprüfen. Dabei stellt sich irgendwann die Frage, unter welchen Umständen diese Prüfung denn vorgenommen werden soll. Dient sie lediglich dazu, während der Entwicklungszeit eines Systems auf die Einhaltung von Kontrakten zu prüfen? Bei ausreichendem Test des Systems könnten Sie die Annahme machen, dass alle Kontraktverletzungen aufgefallen sind und Sie eine solche Überprüfung in einem produktiven System nicht mehr benötigen.

Nicht überprüfbare Bedingungen

Bestimmte Prüfungen können Sie gar nicht sinnvoll in einem produktiven System durchführen, obwohl sie zur Entwicklungszeit durchaus angebracht sind. Ein Beispiel dafür ist der Zugriff auf eine sortierte Liste. Die Suche nach einem Element dieser Liste verursacht Aufwand, der logarithmisch von der Anzahl der Listenelemente abhängt. Es gehört zur Spezifikation unserer Suchmethode, dass sie keinen höheren Aufwand erfordert. Eine sinnvolle Vorbedingung ist es zu fordern, dass die Liste wirklich sortiert ist, weil wir sonst falsche Ergebnisse liefern würden. Aber wir können diese Bedingung zur Laufzeit nicht überprüfen. Die Prüfung selbst hat einen Aufwand, der linear von der Anzahl der Listenelemente abhängt. Wenn Sie die Prüfung durchführen würden, wäre die Spezifikation der Operation von vornherein nicht mehr zu erfüllen. Die Beobachtung verändert in diesem Fall das Beobachtete.

In anderen Fällen kann aber eine Prüfung von Kontrakten zur Laufzeit durchaus sinnvoll sein. Wenn die Konsequenzen der Kontraktverletzung bereits als schwerwiegend absehbar sind, macht auch eine Überprüfung zur Laufzeit Sinn. Im Fall unseres nicht salatöltauglichen LKW war es sicherlich sinnvoll, die Einhaltung des Kontrakts auch im produktiven System zu erzwingen. Dadurch, dass über die Zusicherung ein Betanken des LKW verhindert wurde, haben Sie sich ärgerliche Schadenersatzforderungen aufgrund eines explodierten Dieselmotors erspart.




1  Auch die weiteren Beispiele in diesem Abschnitt stellen wir in der Programmiersprache Java vor.

 << zurück
  
 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Neuauflage: Objektorientierte Programmierung






 Neuauflage:
 Objektorientierte
 Programmierung


Zum Katalog: Java ist auch eine Insel






 Java ist auch
 eine Insel


Zum Katalog: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Katalog: C++ Handbuch






 C++ Handbuch


Zum Katalog: Einstieg in Python






 Einstieg in Python


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo





Copyright © Rheinwerk Verlag GmbH 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de