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

Inhaltsverzeichnis
1 Einleitung
2 Die Basis der Objektorientierung
3 Die Prinzipien des objektorientierten Entwurfs
4 Die Struktur objektorientierter Software
5 Vererbung und Polymorphie
6 Persistenz
7 Abläufe in einem objektorientierten System
8 Module und Architektur
9 Aspekte und Objektorientierung
10 Objektorientierung am Beispiel: Eine Web-Applikation mit PHP 5 und Ajax
A Verwendete Programmiersprachen
B Literaturverzeichnis
Stichwort
Ihre Meinung?

Spacer
 <<   zurück
Objektorientierte Programmierung von Bernhard Lahres, Gregor Rayman
Das umfassende Handbuch
Buch: Objektorientierte Programmierung

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


Rheinwerk Computing - Zum Seitenanfang

7.5 Kontrakte: Objekte als Vertragspartner  Zur nächsten ÜberschriftZur vorigen Überschrift

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 - Zum Seitenanfang

7.5.1 Überprüfung von Kontrakten  Zur nächsten ÜberschriftZur vorigen Überschrift

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 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 sogenannten Zusicherungen (Assertions) eingebürgert.


Icon Hinweis 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 - Zum Seitenanfang

7.5.2 Übernahme von Verantwortung: Unterklassen in der Pflicht  Zur nächsten ÜberschriftZur vorigen Überschrift

Bereits in Abschnitt 5.1.3 haben Sie das Prinzip der Ersetzbarkeit kennengelernt. 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:

  • Unterklassen dürfen die Vorbedingungen für Operationen nicht verschärfen.
  • Unterklassen dürfen die Nachbedingungen einer Operation nicht einschränken.
  • 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 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 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. [Auch die weiteren Beispiele in diesem Abschnitt stellen wir in der Programmiersprache Java vor. ] 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 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 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.

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.

Abbildung 7.52    Radikale Lockerung der Nachbedingung

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 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 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 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.

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); 
    }

Abbildung 7.56    Ablauf beim Betanken eines umgerüsteten Dieselautos

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 - Zum Seitenanfang

7.5.3 Prüfungen von Kontrakten bei Entwicklung und Betrieb  topZur vorigen Überschrift

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.



Ihre Meinung

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.

 <<   zurück
  Zum Rheinwerk-Shop
Neuauflage: Objektorientierte Programmierung






Neuauflage:
Objektorientierte Programmierung

Jetzt Buch bestellen


 Ihre Meinung?
Wie hat Ihnen das Openbook gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Rheinwerk-Shop: Java ist auch eine Insel






 Java ist auch
 eine Insel


Zum Rheinwerk-Shop: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Rheinwerk-Shop: C++ Handbuch






 C++ Handbuch


Zum Rheinwerk-Shop: Einstieg in Python






 Einstieg in Python


Zum Rheinwerk-Shop: IT-Handbuch für Fachinformatiker






 IT-Handbuch für
 Fachinformatiker


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2009
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

Cookie-Einstellungen ändern