Modellierung mit
UML
Loading

3.1 Übersicht über OCL/P

Abbildung 3.1 erläutert die wichtigsten Begriffe der OCL.

Bedingung
Eine Bedingung ist eine boolesche Aussage über ein System. Sie beschreibt eine Eigenschaft, die ein System oder ein Ergebnis besitzen soll. Ihre Interpretation ergibt grundsätzlich einen der Wahrheitswerte true oder false.
Kontext
Eine Bedingung ist in einen Kontext eingebettet, über den sie Aussagen macht. Der Kontext wird definiert durch eine Menge von in der Bedingung verwendbaren Namen und ihren Signaturen. Dazu gehören Klassen-, Methoden- und Attributnamen des Modells und insbesondere im Kontext einer Bedingung explizit eingeführte Variablen.
Interpretation
einer Bedingung wird anhand einer konkreten Objektstruktur vorgenommen. Dabei werden die im Kontext eingeführten Variablen mit Werten beziehungsweise Objekten belegt.
Invariante
beschreibt eine Eigenschaft, die in einem System zu jedem (beobachteten) Zeitpunkt gelten soll. Die Beobachtungszeitpunkte können eingeschränkt sein, um zeitlich begrenzte Verletzungen zum Beispiel während der Ausführung einer Methode zu erlauben.
Vorbedingung
einer Methode charakterisiert die Eigenschaften, die gelten müssen, damit diese Methode ein definiertes und sinnvolles Ergebnis liefert. Ist die Vorbedingung nicht erfüllt, so wird über das Ergebnis keine Aussage getroffen.
Nachbedingung
einer Methode beschreibt, welche Eigenschaften nach Ausführung der Methode gelten. Dabei kann auf Objekte in dem Zustand zurückgegriffen werden, der unmittelbar vor dem Methodenaufruf (zur „Zeit“ der Interpretation der Vorbedingung) gültig war. Nachbedingungen werden anhand zweier Objektstrukturen interpretiert, die die Situationen vor und nach dem Methodenaufruf darstellen.
Methodenspezifikation
ist ein Paar bestehend aus Vor- und Nachbedingung.
Query
ist eine von der Implementierung angebotene Methode, deren Aufruf keine Veränderung des Systemzustands hervorruft. Es dürfen neue Objekte als Aufrufergebnis erzeugt werden. Allerdings dürfen diese nicht mit dem Systemzustand durch Links verbunden sein. Dadurch sind Queries ohne Seiteneffekte und können in OCL-Bedingungen verwendet werden.
Abbildung 3.1: Begriffsdefinitionen zur OCL

Die Möglichkeiten der OCL zur Beschreibung von Bedingungen werden in diesem Abschnitt anhand von Beispielen aus dem Auktionssystem demonstriert. In den nachfolgenden Abschnitten werden diese und weitere OCL-Konstrukte vertieft. Eine vollständige Grammatik der OCL findet sich in Anhang C.3.

3.1.1 Der Kontext einer Bedingung

Eines der herausragenden Merkmale von OCL-Bedingungen ist ihre grundsätzliche Einbettung in einen Kontext, bestehend aus UML-Modellen. Meist ist dieser Kontext durch ein Klassendiagramm gegeben. Das Klassendiagramm in Abbildung 3.2 stellt einen solchen Kontext in Form zweier Klassen und einer Reihe teilweise voneinander abhängiger Attribute bereit, die einen Ausschnitt des Auktionssystems modellieren. Die Attributabhängigkeiten können durch OCL/P-Bedingungen dargestellt werden.

Lädt...
Abbildung 3.2: Ausschnitt des Auktionssystems

Eine einfache Bedingung ist zum Beispiel, dass eine Auktion immer startet bevor sie beendet wird. Diese Bedingung wird durch explizite Erwähnung eines Auktionsobjekts im Kontext beschrieben. Der explizit angegebene Kontext einer OCL-Bedingung besteht aus einem oder mehreren Objekten, deren Name durch die Erwähnung im Kontext in der Bedingung bekannt gemacht wird. Die Bedingung nutzt eine Methode der Klasse Time zum Vergleich:

context Auction a inv:
  a.startTime.lessThan(a.closingTime)

Diese Bedingung benötigt aus ihrem Kontext die Signatur der Klasse Auction. Der Variablenname a wird im Sinne einer Variablenvereinbarung lokal durch die context-Angabe eingeführt. Stattdessen können Variablen auch explizit importiert werden, wenn sie zum Beispiel in einer anderen Bedingung oder einem anderen Diagramm bereits definiert wurden. Mit dieser importierenden Form des Kontexts werden in Kapitel 4 OCL-Bedingungen kombiniert und mit Objektdiagrammen verbunden, denn die in den Objektdiagrammen benannten Objekte können so explizit in den OCL-Bedingungen verwendet werden. Die durch import definierten Kontexte werden deshalb in Kapitel 4 genauer diskutiert. Folgender Kontext charakterisiert zum Beispiel eine Eigenschaft eines Auktionsobjekts a, das aus einem Objektdiagramm stammen kann:

import Auction a inv:
  a.startTime.lessThan(a.closingTime)

Durch Navigationsausdrücke können die in Assoziationen verbundenen Objekte einbezogen werden. Der Ausdruck a.bidder liefert als Ergebnis eine Menge von Personen, das heißt einen Wert vom Typ Set<Person>, von der mit dem Attribut size die Größe bestimmt werden kann. Um sicherzustellen, dass die Anzahl der aktiven Teilnehmer nicht größer als die Anzahl der tatsächlich an einer Auktion teilnehmenden Personen ist, wird folgende Bedingung formuliert:

context Auction a inv Bidders1:
  a.activeParticipants <= a.bidder.size

Einer Bedingung kann wie hier ein eigener Name (Bidders1) gegeben werden, damit darauf an anderen Stellen Bezug genommen werden kann. Navigationsausdrücke können aneinander gereiht werden. Zur Navigation werden jeweils die dem Ausgangsobjekt gegenüberliegenden Rollennamen einer Assoziation verwendet. Ist die Assoziation participants korrekt implementiert, so ist die folgende Bedingung richtig:

context Auction a inv:
  a in a.bidder.auctions

In Verschärfung der vorletzten Bedingung Bidders1 wird nun gefordert, dass die Anzahl der aktiven Teilnehmer einer Auktion genauso groß ist, wie die Anzahl der ihr zugeordneten Personen, deren Attribut isActive gesetzt ist:

context Auction a inv Bidders2:
  a.activeParticipants == { p in a.bidder | p.isActive }.size

Die dabei verwendete Mengenkomprehension ist eine komfortable Erweiterung gegenüber dem OCL-Standard. Sie wird später in mehreren Formen ausführlicher besprochen.

Wird in einem Kontext statt einem expliziten Namen nur die Klasse festgelegt, so wird implizit der Name this als vereinbart angenommen. In diesem Fall kann auf Attribute auch direkt zugegriffen werden. Dies zeigt die folgende Bedingung, in der auch logische Verknüpfungen verwendet werden:1

context Auction inv
  startTime.greaterThan(Time.now()) implies
    numberOfBids == 0

Diese Bedingung ist äquivalent zu der Fassung mit expliziter Verwendung des Namens this:

context Auction inv
  this.startTime.greaterThan(Time.now()) implies
    this.numberOfBids == 0

Geschlossene Bedingungen, wie die Nachfolgende, besitzen einen leeren Kontext. Grundsätzlich kann ein Kontext durch Quantoren ersetzt werden, die über alle existenten Objekte der Klassen aus dem Kontext definiert werden. Die nachfolgende Bedingung mit leerem Kontext ist deshalb äquivalent zur vorhergehenden:

inv:
  forall a in Auction 
    a.startTime.greaterThan(Time.now()) implies
      a.numberOfBids == 0

3.1.2 Das let-Konstrukt

Mithilfe des let-Konstrukts können Zwischenergebnisse einer Hilfsvariablen zugewiesen werden, um diese im Rumpf des Konstrukts gegebenenfalls mehrfach zu nutzen. Nachfolgende Bedingung fordert, dass Start- und Endezeit jeder Auktion in der richtigen Relation stehen, wobei auch das aus Java bekannte if-then-else in der Kompaktform .?.:. zum Einsatz kommt:

context Auction a inv Time1:
  let min = startTime.lessThan(closingTime)
             ? startTime : closingTime 
  in
    min == startTime

Das let-Konstrukt vereinbart lokal benutzbare Variablen und Operationen, die nur innerhalb des Ausdrucks sichtbar sind. Der Typ einer solchen Variable wird durch den rechts gegebenen Ausdruck inferiert, kann aber auch explizit angegeben werden.

In einer let-Klausel können mehrere lokale Variablen und Operationen definiert werden, von denen die nachfolgenden bereits auf die jeweils vorhergehenden zugreifen dürfen.2

context Auction a inv Time2:
  let min1 = a.startTime.lessThan(a.closingTime)
              ? a.startTime : a.closingTime; 
      min2 = min1.lessThan(a.finishTime)? min1 : a.finishTime
  in
    min2 == startTime

Hier werden zwei strukturell gleiche Zwischenergebnisse mit unterschiedlichen Parametern definiert. Um dies zu vereinfachen, kann das let-Konstrukt auch dazu genutzt werden, Hilfsfunktionen zu vereinbaren. Folgendes Beispiel zeigt die Verwendung einer Hilfsfunktion zur Berechnung des Minimums der Zeiten und ist damit äquivalent zur Bedingung Time2:

context Auction a inv Time3:
  let min(Time x, Time y) = x.lessThan(y) ? x : y
  in
    min(a.startTime, min(a.closingTime, a.finishTime))
           == startTime

Bei der Definition einer Hilfsfunktion sind wie in normalen Java-Methoden die Argumente gemeinsam mit ihren Typen anzugeben. Die Operationen sind im funktionalen Stil angegeben, der gleichzeitig dem in Java üblichen objektorientierten Stil für Methoden desselben Objekts entspricht.

Sollte eine Hilfsfunktion öfter benötigt werden, so ist es sinnvoll, diese in einer Klasse des zugrunde liegenden Modells oder in eine eigens dafür bereitgestellte Bibliothek abzulegen. In Abschnitt 3.4.4 wird eine solche Bibliothek diskutiert.

Zwischenvariablen und Hilfsfunktionen können auch als Attribute beziehungsweise als Methoden einer anonymen Klasse verstanden werden, die bei einer let-Konstruktion implizit in den Kontext aufgenommen wird. Dies wird in Abschnitt 3.4.3 nach Einführung der Methodenspezifikation noch an einem Beispiel erläutert. Ein Sonderfall, der im nächsten Abschnitt noch diskutiert wird, besteht jedoch in der Behandlung von undefinierten Ergebnissen. Das let-Konstrukt erlaubt undefinierte Zwischenergebnisse und kann doch in definierter Weise ein Gesamtergebnis erbringen, wenn die Zwischenergebnisse dort nicht auftreten.3

3.1.3 Fallunterscheidungen

Eine Spezifikationssprache besitzt im Gegensatz zu einer imperativen Programmiersprache keine Kontrollstrukturen zur Steuerung des Kontrollflusses. Jedoch werden einige Operationen, die an imperative Konstrukte angelehnt sind, angeboten. Dazu gehört die Fallunterscheidung if-then-else oder die bereits in Bedingung Time1 verwendete äquivalente Form .?.:.. Auf Basis des ersten Arguments wird festgelegt, welches der beiden anderen Argumente evaluiert und als Ergebnis der Fallunterscheidung angenommen wird. Im Gegensatz zur imperativen Fallunterscheidung sind immer der then- und der else-Zweig anzugeben. Beide Zweige beinhalten Ausdrücke mit dem gleichen Datentyp. Die Fallunterscheidung hat dann den gleichen Datentyp wie der erste der beiden Ausdrücke. Das bedeutet, der zweite Ausdruck muss einem Subtyp des ersten Ausdrucks angehören.

Eine spezielle Form der Fallunterscheidung erlaubt die Behandlung von Typkonversionen, wie sie bei Subtyphierarchien gelegentlich auftreten. OCL/P bietet dafür eine typsichere Konstruktion an, die eine Kombination einer Typkonversion und einer Abfrage nach dessen Konvertierbarkeit darstellt. Gemäß Abbildung 2.6 lässt sich aus Geboten der Bieter extrahieren:

context Message m inv:
  let Person p =
        typeif m instanceof BidMessage then m.bidder else null
  in ...

Äquivalent dazu kann die kompaktere Form der Fallunterscheidung gewählt werden:

context Message m inv:
  let Person p = m instanceof BidMessage ? m.bidder : null
  in ...

Zusätzlich zu einer normalen Fallunterscheidung wird bei beiden Formen im then-Zweig der Fallunterscheidung der Typ der Variable m auf BidMessage gesetzt. Die Variable m nimmt diesen Typ dort temporär an und ermöglicht dadurch die Selektion m.bidder. Im Gegensatz zur in Java üblichen Typkonversion mit (BidMessage) ist mit dieser Konstruktion die Typsicherheit gegeben. Das heißt, ein Konversionsfehler, der in Java zu einer Exception und in OCL zu einem undefinierten Wert führen würde, kann nicht auftreten.

3.1.4 Grunddatentypen

Als Grunddatentypen stehen die aus Java bekannten Sorten boolean, char, int, long, float, byte, short und double zur Verfügung. Auch die aus Java bekannten Operatoren wie + oder - werden in OCL/P eingesetzt. Tabelle 3.3 enthält eine Liste der in OCL/P verfügbaren Infix- und Präfixoperatoren inklusive ihrer Prioritäten.4

Priorität Operator Assoziativität Operand(en), Bedeutung




14 @pre links Wert des Ausdrucks in der Vorbedingung
⋆⋆ links Transitive Hülle einer Assoziation
13 +, -, ~ rechts Zahlen
! rechts Boolean: Negation
(type) rechts Typkonversion (Cast)
12 ⋆, /, % links Zahlen
11 +, - links Zahlen, String (+)
10 <<, >>, >>> links Shifts
9 <, <=, >, >= links Vergleiche
instanceof links Typvergleich
in links Element von
8 ==, != links Vergleiche
7 & links Zahlen, Boolean: striktes und
6 ^ links Zahlen, Boolean: xor
5 | links Zahlen, Boolean: striktes oder
4 && links Boolesche Logik: und
3 || links Boolesche Logik: oder
2,7 implies links Boolesche Logik: impliziert
2,3 <=> links Boolesche Logik: äquivalent
2 ? : rechts Auswahlausdruck (if-then-else)
Tabelle 3.3: Prioritäten der OCL-Operatoren

Der Datentyp String wird wie in Java nicht als Grunddatentyp, sondern als standardmäßig zur Verfügung stehende Klasse verstanden. Aus den Java-Klassenbibliotheken und den Packages java.util und java.lang stehen eine Reihe solcher Klassen zur Verfügung.

Eine Sonderrolle unter den Grunddatentypen nimmt der Typ der Wahrheitswerte boolean ein, weil er auch dazu genutzt wird, OCL-Bedingungen zu interpretieren. Da in der (klassischen) Logik nur zwei Wahrheitswerte verwendet werden und die Behandlung nichtterminierender oder abbrechender Interpretation einige Probleme bereitet, wird im folgenden Abschnitt 3.2 eine detaillierte Untersuchung des Zusammenhangs zwischen dem Datentyp boolean und der in der OCL zu verwendenden Logik vorgenommen.


Bernhard Rumpe. Agile Modellierung mit UML. Springer 2012