View on GitHub

Software Engineering - HS 2024

Vorlesungswebseite für den Kurs Software Engineering der Universität Basel

Logische Spezifikationen mit der Objekt Constraint Language (OCL)

In der letzten Woche haben wir gesehen, wie wir mithilfe von logischen Ausdrücken Vor- und Nachbedingungen sowie Invarianten, Eigenschaften von Modulen spezifizieren können. Obwohl wir solche formalen Spezifikationen nicht routinemässig für jedes Modul einsetzen, sind sie doch ein wichtiges Werkzeug, um die Spezifikationen zu präzisieren und zu verifizieren. Entsprechend bietet auch die UML eine Möglichkeit, solche Spezifikationen zu erstellen, nämlich die Object Constraint Language (OCL). In diesem Artikel wollen wir uns die wichtigsten Aspekte der OCL anhand von einem Beispiel ansehen, ohne jedoch ins Detail zu gehen. OCL selbst ist eine relativ komplexe Sprache, die wir hier nicht im Detail behandeln können.

(Das Nachfolgende Beispiel ist dem Wikipedia Eintrag zum Thema OCL entnommen)

Das Klassendiagramm

Der Startpunkt unserer Spezifikation ist ein Klassendiagramm.

Klassendiagramm

Wenn Sie sich dieses Klassendiagramm ansehen, erhalten Sie bereits eine gute Vorstellung davon, wie die Klasse Person spezifiziert ist und wie die Beziehung zur Klasse Auto aussieht. Zum Beispiel können wir ablesen, dass eine Person mehrere Kinder haben kann, und selbst jeweils genau 2 Eltern hat. Eine Person kann ein oder mehrere Autos besitzen, und ein Auto gehört genau einer Person. Es gibt aber auch einige Dinge, die nicht explizit spezifiziert sind. Beispielsweise wissen wir zwar, dass das alter einer Person eine Ganzzahl sein muss, es ist aber nirgends spezifiziert, dass das Alter einer Person positiv sein muss. Die Spezifikation würde auch zulassen, dass die Eltern jünger als die Kinder sind, oder dass eine Person, die erst 5 Jahre alt ist ein Auto besitzt. Genau solche Dinge können wir mit OCL spezifizieren und damit die Spezifikation präzisieren.

Spezifikation mit OCL

Die OCL sieht drei Arten von Ausdrücken vor, nämlich Invariante, Vorbedingungen und Nachbedingungen. Wir beginnen mit den Invarianten.

Invarianten

Am besten wir starten gleich mit einem Beispiel, welches Sie sicher mühelos verstehen können:

context Person inv: self.alter >=0

Um eine Invariante zu spezifizieren, schreiben wir das Schlüsselwort context sowie den Klassennamen, um anzugeben, auf welche Klasse sich die Invariante bezieht. Das Schlüsselwort inv nach dem Klassennamen gibt an, dass es sich um eine Invariante handelt. Die Invariante selbst ist ein logischer Ausdruck, der für alle Instanzen der Klasse gelten muss. In diesem Fall besagt die Invariante, dass das Alter einer Person immer grösser oder gleich 0 sein muss. Das reservierte Wort self bezieht sich auf die Instanz (also das Objekt), für die die Invariante gilt. Es entpricht der this Referenz in Java.

Nun schauen wir uns ein etwas komplexeres Beispiel an. Wir wollen ausdrücken, dass eine Person jünger ist als ihre Eltern. Dies erreichen wir mit folgendem Ausdruck:

context Person inv: self.eltern->forAll(e|e.alter>self.alter)

Der Ausdruck self.eltern gibt uns eine Collection von Objekten vom Typ Person. Die Pfeilnotation -> ist eine Navigationsnotation, die uns erlaubt, auf die Attribute und Methoden der Objekte in der Collection zuzugreifen. Die Funktion forAll wendet die Funktion, die nach dem | Zeichen steht auf alle Elemente der Collection an. In diesem Fall ist die Funktion e|e.alter>self.alter. Diese Funktion gibt true zurück, wenn das Alter des Elements e grösser ist als das Alter der Person, für die die Invariante gilt. Die Invariante ist also erfüllt, wenn alle Eltern älter sind als die Person selbst.

Die Nachfolgenden zwei Ausdrücke sollten Sie nun selbst verstehen können.

context Person inv: self.alter<18 implies self.autos->isEmpty()
context Person inv: Person.allInstances()->exists(p | p.autos->size() > 0)

Der erste Ausdruck sagt, dass eine Person, die jünger als 18 ist, keine Autos besitzen darf. Der zweite Ausdruck sagt, dass es mindestens eine Person geben muss, die ein Auto besitzt.

Vor- und Nachbedingungen

Wenn wir eine Methode spezifizieren, können wir auch Vor- und Nachbedingungen angeben. Die Vorbedingungen geben an, welche Bedingungen vor dem Aufruf der Methode gelten müssen, damit die Methode aufgerufen werden kann. Die Nachbedingungen geben an, welche Bedingungen nach dem Aufruf der Methode gelten müssen.

Auch dies lässt sich am besten anhand eines Beispiels illustrieren:

context Person::bekommtKind() 
    pre: self.female = true
    post: self.kinder->notEmpty() and self.kinder->size() > self.kinder@pre->size()
context Person::bekommtKind() post: self.kinder->notEmpty() and self.kinder->size() > self.kinder@pre->size()

Die Vorbedingung self.female = true besagt, dass nur Frauen Kinder bekommen können. Die Nachbedingung self.kinder->notEmpty() and self.kinder->size() > self.kinder@pre->size() gibt an, dass die Person nach dem Aufruf der Methode mindestens ein Kind haben muss, und dass die Anzahl Kinder grösser sein muss als vor dem Aufruf der Methode.

Schlussbemerkung

Wenn Sie diesen Beispiel aufmerksam gefolgt sind, sollten Sie das Beispieldiagramm nun besser verstehen. Vielleicht sind Ihnen sogar Ungereimtheiten aufgefallen? Sollte die Methode bekommtKind wirklich nur von Frauen aufgerufen werden können? Bezieht sich das Kind bekommen auf die Person selbst, oder auf die Person und ihren Partner? Solche Fragen können Sie sich aber nur stellen, da Sie nun eine präzise Spezifikation haben dank derer Ihnen solche Feinheiten überhaupt auffallen.