Definition
Design by Contract
Design by Contract (DbC) ist ein Konzept der Softwareentwicklung, bei dem Methoden und Klassen als Vertragspartner betrachtet werden. Ein solcher Vertrag legt fest:
-
Vorbedingungen (Preconditions): Was der Aufrufer einer Methode garantieren muss.
-
Nachbedingungen (Postconditions): Was die Methode garantiert, wenn die Vorbedingungen erfüllt sind.
-
Invarianten: Bedingungen, die während der gesamten Lebensdauer eines Objekts stets erfüllt sein müssen.
Ziel ist es, klare Verantwortlichkeiten zwischen Aufrufer und Methode zu definieren und robuste, fehlerresistente Software zu entwickeln.
Beispiele
Beispiel 1
public class Kreis { private double radius; public Kreis(double radius) { if (radius <= 0) throw new IllegalArgumentException("Radius muss positiv sein"); this.radius = radius; } public void vergroessern(double faktor) { if (faktor <= 0) throw new IllegalArgumentException("Faktor muss positiv sein"); radius *= faktor; } public double berechneFlaeche() { return Math.PI * radius * radius; } }
Vorbedingungen:
Der Parameter radius im Konstruktor muss positiv sein
Der Parameter faktor in der Methode vergroessern() muss positiv sein
Nachbedingungen:
Die Methode vergroessern() hat den Radius um exakt den angegebenen Faktor vergrößert.
Invarianten:
Die Bedingung radius > 0 gilt jederzeit, egal welche Methode gerade ausgeführt wurde.
Beispiel 2
Stellen wir uns eine Warteschlange vor, die nach dem FIFO-Prinzip arbeitet (first in, first out). Eine Klasse Queue stelt nun folgende Methoden zur Verfügung:
- public Queue (int capacity) // Konstruktor
- public void enqueue(String value)
- public void dequeue()
- public String front()
- public int size()
Vorbedingungen:
- Konstruktor: Der Parameter capacity muss > 0 sein
- enqueue(): Die Kapazität darf noch nicht erreicht sein
- dequeue(): Die Warteschlange darf nicht leer sein
- front(): Die Warteschlange darf nicht leer sein
Nachbedingungen:
- Konstruktor: Es wurde eine Queue mit capacity Plätzen erzeugt
- enqueue(): Die Anzahl der Elemente wurde um 1 erhöht und das neue Element befindet sich am Ende der Schlange
- dequeue(): Die Anzahl der Elemente wurde um 1 erniedrigt, das zuerst eingefügte (vordere) Element wurde entfernt.
- front(): Die Anzahl der Elemente wurde nicht verändert, das zuerst eingefügte (vordere) Element wird als Wert zurückgegeben.
- size(): Die Zahl der vorhandenen Elemente wird zurückgegeben und die Zahl der Elemente wurde nicht verändert.
Invarianten:
Egal, welche Methode gerade ausgeführt wurde, gilt:
- 0 <= n <= capacity // n = Anzahl der vorhandenen Elemente
- Alle belegten Felder liegen im Bereich 0 bis capacity - 1, wenn die Queue mithilfe eines Arrays implementiert wurde.
- Die FIFO-Reihenfolge bleibt erhalten
Dieses zweite Beispiel zeigt sehr gut, wie wichtig es ist, dass der interne Zustand eines Objektes wie einer Queue immer konsistent bleibt, unabhängig davon, wie oft enqueue(), dequeue() oder eine der anderen Methoden ausgeführt wurde. Gerade bei Datenstrukturen, die Abstrakte Datentypen verwirklichen (Stack, Queue, List etc.) sind Invarianten eine Garantie für Konsistenz.
Quellen:
- Lahres et al.: Objektorientierte Programmierung, Rheinwerk Computing 2021.
- Barnes, Kölling: Java lernen mit BlueJ - Objects first. Pearson-Verlag 2019.
- Ullenboom: Java ist auch eine Insel, Rheinwerk Computing 2023.