Modellierung mit
UML
Loading

3.2 Die OCL-Logik

Die richtige Definition der einer Bedingungssprache zugrunde liegenden Logik ist für einen praxistauglichen Einsatz der Sprache wichtig. Deshalb werden in diesem Abschnitt zunächst die möglichen Varianten an einem Beispiel vorgestellt und dann die OCL-Logik definiert.

3.2.1 Die boolesche Konjunktion

In Java kann ein Ausdruck vom Typ boolean drei verschiedene „Werte“ haben: Er kann zu den definierten Werten true oder false evaluieren oder einen undefinierten Pseudowert haben. Dieser Pseudowert modelliert, dass eine Exception geworfen wird oder die Berechnung nicht terminiert. Es gibt nun mehrere Möglichkeiten, mit diesem dritten Wert umzugehen. Anhand der möglichen Semantiken der booleschen Konjunktion (&&) lässt sich dies sehr gut diskutieren. Abbildung 3.4 zeigt fünf verschiedene Möglichkeiten, die Konjunktion zu verstehen. Bei dieser Darstellung wird angenommen, dass der Pseudowert durch undef dargestellt wird. Die fünf angegebenen Interpretationen für die Konjunktion unterscheiden sich jeweils im Umgang mit diesem Pseudowert.

Lädt...
Abbildung 3.4: Interpretationen der OCL-Konjunktion

Soll der klassische zweiwertige Fall 3.4(a) erhalten bleiben, so sind die Wahrheitswerte der Logik und der boolesche Datentyp strikt voneinander zu trennen. Das Beispiel CIP [BBB+85] zeigt, dass dies zu einer Verdopplung der Logik-Operatoren führt und damit für die Praxis unhandlicher wird. Daneben gibt es vier verschiedene sinnvolle Möglichkeiten, den &&-Operator auf den undefinierten Wert zu erweitern.

Für die Logik ist die Abbildung des undefinierten Werts auf den Wahrheitswert false am einfachsten, weil damit de’facto wieder eine zweiwertige Logik entsteht (Fall 3.4(e)). Sowohl Spezifikationen als auch Beweisführungen werden dadurch besonders einfach, weil ein dritter Fall nicht existiert und in Fallunterscheidungen damit nicht beachtet werden muss. Dies ist immerhin eine beachtliche Reduktion von neun auf vier zu bedenkende Fälle. Unglücklicherweise ist diese, für die Spezifikation so angenehme Semantik der Konjunktion nicht vollständig implementierbar, da zu bestimmen wäre, ob eine Berechnung nicht terminiert, und dann der Wert false auszugeben wäre.5

Demgegenüber sind alle anderen Semantiken 3.4(b,c,d) implementierbar und finden auch praktische Anwendung. Die strikte Implementierung (b) liefert bereits dann keinen definierten Wert, wenn eines der Argumente keinen liefert. Dies entspricht dem Java-Operator &, der immer beide Argumente auswertet. Dieser Operator ist jedoch langsam und für viele Bedingungen auch ungeeignet, denn in Java wirkt der erste Ausdruck oft als Wächter für den zweiten, der nur ausgewertet werden soll, wenn der erste true ergibt. Diese Auswertungsreihenfolge drückt sich in der Unsymmetrie der sequentiellen Konjunktion aus, die mit dem Java-Operator && bei der Programmierung genutzt werden kann. Beispielsweise ist für ein Auktionsobjekt a

a.bestBid != null && a.bestBid.value > 0

ein Java-Ausdruck, der immer ausgewertet werden kann. Die sequentielle Konjunktion ist für Programmierzwecke ein guter Kompromiss zwischen Auswertungseffizienz und Beschreibungsmächtigkeit. Sie hat jedoch für die Logik den gravierenden Nachteil, dass

&& y <=> y && x

im Allgemeinen nicht gilt und damit ein Refactoring (also eine den Gesetzen der Logik genügende Umformung) deutlich erschwert wird.

Im UML-Standard wird für die Semantik der Konjunktion die Kleene-Logik vorgeschlagen (Fall (c)) und zum Beispiel in [BW02aBW02b] in HOL formalisiert. Darin wird angenommen, dass ein mit false belegtes Argument ausreicht, um dem gesamten Ausdruck den Wert false zu geben. Die Kleene-Logik hat den sehr angenehmen Vorteil, dass wesentliche Gesetze der booleschen Logik, wie Kommutativität und Assoziativität der Konjunktion, weiter gelten.6 In ihr kann die Konjunktion aber nur aufwändig implementiert werden, indem beide Argumente parallel ausgewertet werden. Terminiert dann eine Auswertung mit false, so ist die andere Auswertung abzubrechen. Diese Implementierungsform ist für Programmiersprachen wie Java leider aufwändig.

3.2.2 Zweiwertige Semantik und Lifting

Aufgrund der Überlegungen des vorangegangenen Abschnitts stellen sich für die Semantik von Ausdrücken einer Spezifikationssprache folgende Fragen:

  1. Welche Semantik wird für die Logikoperatoren gewählt?
  2. Welche booleschen Gesetze bleiben erhalten oder werden verletzt?
  3. Stimmen die offizielle (denotationelle) Semantik und die in einem Werkzeug implementierte Auswertungsstrategie überein? Welche Auswirkungen haben Unterschiede?

Für die möglichst abstrakte Spezifikation von Funktionalität eignet sich die zweiwertige Logik am besten. Sie zwingt als einzige den Modellierer nicht zur fortwährenden Beachtung des dritten, undefinierten Falls. Für den Umgang mit undefinierten Teilausdrücken wird deshalb das Lifting zur Semantik von OCL-Bedingungen eingeführt.

Das bedeutet, ein OCL-Ausdruck wird auch als nicht erfüllt interpretiert, wenn er den Pseudowert undef liefert. Damit ist der OCL-Operator && aber nicht implementierbar und eine korrekte Code-Generierung aus OCL-Ausdrücken nicht möglich. Dies ist für Testverfahren und Simulationen ein Problem. Aber es erweist sich, dass dieses Problem in der Praxis nur selten eine Rolle spielt, denn in der Programmiersprache Java gibt es nur zwei Arten von Situationen, in denen sich der Pseudowert undef manifestiert. Zum einen können Exceptions geworfen werden, zum anderen kann es sich um eine nichtterminierende Berechnung handeln.

Im ersten Fall kann diese Exception abgefangen und mit false bewertet werden. Beispielsweise kann die Bedingung a&&b zu folgendem Codestück umgesetzt werden:

boolean res;
try {
  res = a;       // Auswertung Ausruck a
catch(Exception e) {
  res = false;
}
if(res) {        // Effizienz: b nur auswerten, wenn a wahr
  try {
    res = b;     // Auswertung Ausdruck b
  } catch(Exception e ) {
    res = false;
  }
}

Nichtterminierende Berechnungen kommen in der objektorientierten Praxis relativ selten vor. Offensichtlich lassen sich nichtterminierende Berechnungen, wie unendliche while-Schleifen, relativ leicht vermeiden. Weniger offensichtliche Situationen, wie nichtterminierende Rekursionen, führen meist durch die Beschränktheit der Ressourcen in Java zu Exceptions (zum Beispiel Stack Overflow). Deshalb lässt sich zusammenfassend feststellen, dass eine für pragmatische Zwecke ausreichende, fast zur zweiwertigen Semantik identische Auswertungsstrategie für OCL-Ausdrücke existiert. Deshalb wird nach diesem Exkurs in die Auswertbarkeit von OCL-Ausdrücken die Semantik für boolesche Operatoren gemäß den Wahrheitstabellen aus Abbildung 3.5 festgelegt. Es wird also in Übereinstimmung mit [HHB02] gefordert, für OCL eine zweiwertige Logik zu verwenden.

Lädt...
Abbildung 3.5: Die booleschen Operatoren

Für diese Semantikdefinition gibt es einen alternativen Ansatz zur Erklärung durch die Benutzung eines Liftingoperators , der auf jedes Argument eines booleschen Operators angewandt wird. Dieser Operator hat die Eigenschaft undef==false während sonst true==true und false==false. Damit entspricht a&&b dem Ausdruck (a)&&(b) und die booleschen Operatoren müssen nur zweiwertig sein. Der Liftingoperator muss in der OCL nicht explizit zur Verfügung stehen, da er von einem Parser implizit hinzugefügt werden kann. Der Liftingoperator kann jedoch nicht vollständig implementiert werden. Ein Workaround basiert wie oben beschrieben auf dem Abfangen von Exceptions.

3.2.3 Kontrollstrukturen und Vergleiche

Einige von der OCL/P angebotenen Operatoren beschreiben Vergleiche zwischen Objekten beziehungsweise Werten. Genau wie der Datentyp boolean enthalten alle Datentypen einen undefinierten Pseudowert. Deshalb werden Vergleichsoperatoren auf den undefinierten Wert erweitert. Dabei wird aus Komfortgründen festgelegt, dass bis auf die bereits beschriebenen Logikoperatoren, die beiden Fallunterscheidungen und das let-Konstrukt alle OCL-Konstrukte auf undefinierten Werten immer strikt sind, also immer undefiniert liefern, wenn sie ein undefiniertes Argument erhalten. Insbesondere ist a==b undefiniert. Das heißt auch wenn beide Ausdrücke a und b undefiniert sind, sind sie nicht gleich. Abbildung 3.6 zeigt die Definition der Fallunterscheidungen.

Lädt...
Abbildung 3.6: Fallunterscheidung in zwei syntaktischen Formen

Jedes der beiden Argumente des then- und des else-Zweiges kann undefinierte Werte haben, die sich jedoch nicht auswirken, wenn der jeweils andere Zweig ausgewählt und evaluiert wird. Auch die boolesche Bedingung der Fallunterscheidung darf undefiniert sein. In diesem Fall wird der else-Zweig ausgeführt.

Der in der OCL verwendete Vergleich == ist mit der oben beschriebenen Konvention identisch zu dem in Java zur Verfügung stehenden Vergleich ==. Er vergleicht auf Grunddatentypen die Werte und auf Objektklassen die Identität der Objekte. Daneben steht in OCL der vom Datenmodell zur Verfügung gestellte Vergleich equals() als normale Query zur Verfügung. Die Verwendung von Vergleichen auf Containern wird im nachfolgenden Abschnitt besprochen.

Die Striktheit des Vergleichs == auf allen Datentypen hat einen für die Logik nicht so angenehmen Nebeneffekt, der andererseits den Umgang mit dem undefinierten Pseudowert verbessert. Es gilt im Allgemeinen nicht, dass

(a == b) || (a != b),

denn ist einer der beiden Ausdrücke undefiniert, so evaluieren beide Seiten der Disjunktion zu false. Damit können aber in der OCL-Logik undefinierte Werte erkannt werden, denn es ist !(a==a) genau dann wahr, wenn a undefiniert ist. Dieser Effekt wird für den in Abschnitt 3.3.11 eingeführten defined-Operator als charakterisierende Eigenschaft verwendet.


Bernhard Rumpe. Agile Modellierung mit UML. Springer 2012