Agile Modellierung mit
UML
Loading

5.3 Codegenerierung aus OCL

Auf Basis der bisher beschriebenen Möglichkeiten, die OCL als Spezifikationssprache für UML/P-Programme zu nutzen, gibt es zum Beispiel die Möglichkeit, die OCL zur Modellierung von Invarianten in Datenbanksystemen einzusetzen [DH99] oder mit der OCL die Geschäftslogik zu beschreiben [DHL01]. Die dabei zugrunde liegende Semantik der OCL ist in ihrer Essenz mit der bisher diskutierten Bedeutung der OCL identisch, jedoch ist die Einsatzform und damit die Abbildung der OCL in ausführbaren Code wesentlich anders. Statt Klassen und Objekten stehen in relationalen Datenbanken Entitäten und Zeilen zur Verfügung, über denen OCL-Ausdrücke interpretiert werden.

Mehrere, teils prototypische Werkzeuge bieten bereits Realisierungen für OCL-Codegeneratoren an. In [HDF00] wird eine modulare Architektur für die OCL vorgestellt, die als Grundlage für eine Integration mit anderen UML-Werkzeugen gut geeignet ist [BS01aBBWL01]. Eine der OCL verwandte Sprache, genannt „Java Interface Specification Language“ (JISL), wird in [MMPH99] zur Methodenspezifikation vorgestellt und diskutiert, wie die Ausführbarkeit der Spezifikation durch eine Transformation nach Java vorgenommen werden kann.

Weil zwischen OCL/P und Java aufgrund der syntaktischen und semantischen Nähe relativ viele Konzepte eindeutig nach Java umgesetzt werden, sollen nachfolgend vor allem die interessanten OCL-Konstrukte diskutiert werden. Dazu gehören unter anderem die Umsetzung der Kontextdefinition, der OCL-Logik, der Komprehension, der Navigation, der Quantoren und der Spezialoperatoren.

Viele, jedoch nicht alle der nachfolgend beschriebenen Codestücke lassen sich in wiederverwendbare Methoden kapseln. Es wird hier auf die Darstellung der Kapselung verzichtet, da dies nur den dargestellten Code vergrößert. Bei einer manuellen Verwendung dieser Codestücke (als eine Art von Entwurfsmuster) ist allerdings eine Kapselung zu empfehlen.

5.3.1 OCL-Aussage als Prädikat

Obwohl OCL-Bedingungen, wie in Abschnitt 4.1.2 beschrieben, in sehr eingeschränkter Form in Implementierungscode umgesetzt werden können, hat eine OCL-Aussage eine natürliche Bedeutung als Prädikat. Deshalb werden OCL-Aussagen, wie etwa

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

in boolesche Methoden übersetzt. Dabei gibt es zwei Varianten. Die Interpretation als Invariante produziert:

       Java/P   
 public static boolean invariantBidders1() {
  boolean res = true;
  Set<Auction> auctions = Auction.getAllInstances();
  for(Iterator<Auction> ia = auctions.iterator();
                             ia.hasNext() && res; ) {
   Auction a = ia.next();
   res &= a.activeParticipants <= a.bidder.size;
  }
  return res;
}

Die generierte Methode prüft die Invariante Bidders1 für alle Objekte der Klasse Auction. Um dies zu ermöglichen, ist eine Infrastruktur zur Verwaltung der Menge aller Auktionsobjekte erforderlich, die zum Beispiel durch die Methode getAllInstances zur Verfügung gestellt wird.

Tatsächlich ist für praktische Anwendungen ein Einsatz der OCL-Bedingung in der oben beschriebenen Form ungünstig. Im laufenden System werden seit der letzten Prüfung der Invariante normalerweise nur relativ wenige Auktionsobjekte modifiziert worden sein. Deshalb ist es unter Umständen günstig, zusätzliche Infrastruktur zu investieren, um die Invariantenprüfung effizienter zu gestalten. Dies kann erreicht werden, indem die Prüfung der Invariante explizit an bestimmten Stellen gefordert wird. Dann sind aber nicht alle, sondern nur bestimmte Auktionsobjekte zu testen. Die dafür geeignete Methode ist

       Java   
 public static boolean checkBidders1(Auction a) {
  return a.activeParticipants <= a.bidder.size();
}

und ihre der Klasse Auction zugeordnete Variante

       Java   
 class Auction ... {
  public boolean checkBidders1() {
    return this.activeParticipants <= this.bidder.size();
  }

Alle diese Formen eignen sich zum Einsatz in Tests, in denen die Prüfung von Invarianten explizit gefordert werden kann. Zu beachten ist, dass nach Konvention die vollständige Invariante das Präfix invariant besitzt, während check für die Einzelfall-Prüfung verwendet wird. Die explizite Übergabe des Kontexts einer OCL-Aussage in Form der zu prüfenden Objekte wird normalerweise durch das Schlüsselwort import vorgenommen. Deshalb werden die letzten beiden Methoden auch aus folgender Bedingung erzeugt:

       OCL  
 import Auction a inv Bidders1:
  a.activeParticipants <= a.bidder.size

Da in der Praxis ein mit dem Schlüsselwort context geschlossener Kontext oft auch auf einzelne Objekte angewandt werden soll, werden daraus sowohl eine geschlossene als auch zusätzlich parametrisierte Methoden erzeugt. Besteht die Kontextangabe aus mehreren Objekten, so werden alle als Parameter verwendet. Folgende Tabelle 5.18 beschreibt die Transformation:



OCL-Kontext: Umsetzung des Kontexts einer Bedingung


Erklärung

Der Kontext wird in OCL explizit in Form von Objekten angegeben. Diese wirken als Parameter bei den daraus generierten booleschen Methoden.


Kontext

       OCL  
   
 context Classcontext inv Name:
  Body
   

       Java/P         
public static boolean invariantName() {
  boolean res = true;
  // Iteration nur einmal dargestellt
  Set<Class> s = Class.getAllInstances();
  for(Iterator<Class> it = s.iterator();
                   it.hasNext() && res; ) {
   Class ob = it.next();
   res &= Body’;
  }
  return res;
}
public static boolean checkName(Classcontext) {
  Body’
}

 

  • Die Iteration wird für jedes Element Class ob des Kontexts geschachtelt wiederholt. Entsprechend ergeben sich polynomiale Komplexitäten bei der Prüfung der vollständigen Invariante.

  • Der generierte Code ist in einer statischen Methode abgelegt und wird einer geeigneten Klasse zugeordnet. Besteht der Kontext aus nur einem Objekt, so kann zusätzlich eine nicht-statische Methode in der Klasse des Objekts generiert werden.

  • Die Methode getAllInstances wird nachfolgend beschrieben.

  • Das alternative Schlüsselwort import generiert nur die parametrisierten Formen.

  • Der Rumpf der Bedingung Body wird entsprechend der nachfolgenden Regeln in Body’ umgesetzt.



Tabelle 5.18.: OCL-Kontext: Umsetzung des Kontexts einer Bedingung

5.3.2 OCL-Logik

Wie in Abschnitt 3.2.2, Band 1 besprochen ist die OCL-Logik zweiwertig und nutzt einen impliziten Liftingoperator, um undefinierte Ergebnisse als false zu interpretieren. Der mit bezeichnete Liftingoperator ist wegen seiner Eigenschaft undef==false nicht (vollständig) implementierbar. Jedoch existiert eine bereits in Abschnitt 3.2.2, Band 1 diskutierte Umsetzung dieses Operators in Java, die Exceptions abfängt und so nur nichtterminierende Berechnungen nicht erkennen kann. Der Liftingoperator ist implizit bei allen booleschen Operationen der OCL zu finden, so dass die Umsetzung der booleschen Operationen &&, ||, !, implies und <=> jeweils der Verwendung des Liftings bedarf.



OCL-Logik: Logikoperatoren


Erklärung

Die zweiwertige Logik wird mittels Lifting des undefinierten Pseudowerts undef auf false umgesetzt.


Negation

... !a ...
   

       Java   
   
 boolean res;
try {
  res = a;
} catch(Exception e) {
  res = false;
}
... !res ...

 

  • Die Einbettung der Auswertung des booleschen Ausdrucks a in eine catch-Anweisung macht eine Zerschlagung des Kontexts von a notwendig. Der Teilausdruck a wird vorab berechnet und in der Ergebnisvariable res zwischengespeichert. Die Umordnung der Berechnung ist erlaubt, da Seiteneffekte in OCL-Ausdrücken nicht auftreten.

  • Eine Nichtterminierung der Auswertung von a wird nicht erkannt.


Äquivalenz

... a <=> b ...
   

       Java   
   
 boolean resa, resb;
try {
  resa = a;
} catch(Exception e) {
  resa = false;
}
try {
  resb = b;
} catch(Exception e) {
  resb = false;
}
... (resa==resb) ...

Weitere Operatoren

werden in analoger Form umgesetzt. Implikation a implies b wird auf !a || b, Konjunktion und Disjunktion werden auf die jeweiligen Java-Operatoren abgebildet.



Tabelle 5.19.: OCL-Logik: Logikoperatoren

Zur Umsetzung der OCL-Kontrollstrukturen können die entsprechenden Java-Kontrollanweisungen eingesetzt werden. Das let-Konstrukt führt lokale Variablen ein und fängt bei der Berechnung der Variablenwerte auftretende Exceptions ab.

5.3.3 OCL-Typen

Die OCL/P-Grunddatentypen stimmen mit denen aus Java überein, so dass eine Umsetzung nicht erforderlich ist. Auch der Vergleich der Grunddatentypen == sowie die arithmetischen Operationen werden unverändert übernommen.

Die Container Set<X>, List<X> und Collection<X>, die die OCL anbietet, stellen eine Teilmenge der Container aus Java dar. Deshalb können die OCL-Container nahezu unverändert nach Java übernommen werden. Die explizite Aufzählung wird im Wesentlichen auf einen Konstruktor und elementweises Hinzufügen abgebildet. Dabei wird vom Generator festgelegt, welche konkrete Mengen- oder Listenimplementierung verwendet wird.

Die Verwendung von Containern über Grunddatentypen bedarf einer besonderen Behandlung. Java ist im Gegensatz zur OCL nicht in der Lage, zum Beispiel int-Werte direkt in Containerstrukturen zu speichern. Stattdessen ist die objektifizierte Form „Integer“ zu verwenden. Der Typ Set<int> wird also nach Set<Integer>überführt. Das in Java vorhandene Boxing von Werten in ihre objektivierten Fassungen erlaubt es allerdings Anwendungsstellen unverändert zu lassen.

Da für Container nicht die Objektidentität, sondern der inhaltliche Vergleich von Interesse ist, wird der Vergleich == in eine eigens generierte Methode übersetzt, die Gleichheit auf Containern wie in Abschnitt 3.3.3, Band 1 beschrieben interpretiert.

Die Umsetzung der in der OCL/P eingeführten Komprehensionskonstrukte wird in der folgenden Tabelle 5.20 erklärt.



OCL-Komprehension: Umformung der Komprehension


Erklärung

Die für Mengen und Listen verwendeten Komprehensionsformen werden in Java durch zusätzliche Infrastruktur ergänzt.


Aufzählung

       OCL        
Set{ a..b }
   

       Java/P   
 Set<Integer> res = ...
for(int i = a; i <= b; i++)
  res.add(i);
}
// Weiterverwendung von res

 

  • Die Aufzählung wird durch eine Schleife realisiert, da Java keine Kompaktform anbietet.

  • Die Variable res erlaubt die Weiterverwendung für den Aufbau einer Menge aus mehreren Aufzählungen, zum Beispiel der Form Set{a,b..c,d..e}.

  • res ist zum Beispiel mit new HashSet() zu besetzen.


Komprehension mit Generator

       OCL        
Set{ expr | var in expr2 }
   

       Java/P         
Set<Class> res = new HashSet<Class>;
for(Iterator<Class2> it = expr2’.iterator();
                           it.hasNext(); ) {
  Class2 var = it.next();
  res.add(expr’);
}
// Weiterverwendung von res

 

  • Die Berechnung des OCL-Ausdrucks wird in mehrere Anweisungen mit Zwischenergebnis res aufgebrochen. Die Umordnung der Berechnung ist erlaubt, da OCL und auch seine Umsetzung seiteneffektfrei ist.

  • Die Ausdrücke expr und expr2 werden ebenfalls transformiert.

  • expr hat den Typ Class.

  • expr2 hat den Typ Set<Class2>.


Komprehension mit Filter

       OCL        
Set{ expr | var in expr2, boolexpr }
   

       Java/P         
Set<Class> res = new HashSet<Class>;
for(Iterator<Class2> it = expr2’.iterator();
                           it.hasNext(); ) {
  Class2 var = it.next();
  if(boolexpr)
    res.add(expr’);
}
// Weiterverwendung von res

 

  • Wie in Abschnitt 3.3.2, Band 1 beschrieben, entfernt ein Filter boolexpr Elemente aus der Menge.

  • Der Filter gewinnt seine Mächtigkeit in Kombination mit dem bereits formulierten Generator. Deshalb wird die Transformation mit angefügtem Generator dargestellt.


Lokale

Variablen

       OCL        
Set{ expr | var in expr2, Type x = expr3 }
   

       Java/P         
Set<Class> res = new HashSet<Class>;
for(Iterator<Class2> it = expr2’.iterator();
                           it.hasNext(); ) {
  Class2 var = it.next();
  Type x = expr3;
  res.add(expr’);
}
// Weiterverwendung von res

 

  • Eine lokale Variablendefinition wirkt wie ein let-Konstrukt.


Kombination

von Generatoren, lokalen Variablendefinitionen und Filtern innerhalb von Komprehensionen ist beliebig möglich. Bei mehreren Generatoren steigt jedoch die Komplexität schnell an.



Tabelle 5.20.: OCL-Komprehension: Umformung der Komprehension

Die Komprehension ist grundsätzlich verwandt mit SQL-Statements, wenn auch ausdrucksstärker. Eine Übersetzung von OCL in eine Datenbankanfrage kann zumindest teilweise die Effizienz der Datenbankanfragen nutzen.

5.3.4 Typ als Extension

Die OCL erlaubt die Verwendung von Typausdrücken wie Auction als Extension, die damit gleichzeitig die Menge der aktuell existierenden Objekte dieses Typs beschreibt. So kann zum Beispiel formuliert werden:

       OCL  
   
 forall a in Auction: a.person.size < 500

Um eine Überprüfung dieser Bedingung zur Laufzeit zu erlauben, ist der Zugriff auf alle zu einem Zeitpunkt existenten Objekte des Typs Auction erforderlich. Deshalb ist eine entsprechende Infrastruktur zur Verwaltung der Extensionen der Objekte zur Verfügung zu stellen. Diese kann auf Basis von WeakHashMaps realisiert sein, die mit der Garbage Collection zusammenarbeiten.

Eine boolesche Variable OCL.oclmode beschreibt, ob das System gerade eine OCL-Bedingung auswertet und daher eventuell neu angelegte Objekte nicht zur Extension hinzuzurechnen sind13

In der Verwaltung sind zusätzliche Maßnahmen zu treffen, um die in den nachfolgend besprochenen Methodenspezifikationen möglichen Konstrukte Class@pre und new(.) zur Charakterisierung neuer Objekte zu evaluieren. Da Methodenaufrufe geschachtelt sein können, wird der Aufrufkeller bei der Speicherung der Instanzmengen nachgebildet, um so jeder Nachbedingung den Zugriff auf den jeweiligen Zustand zur Vorbedingung zu ermöglichen. Wesentlich ist hier die effiziente Verwaltung und Optimierung in Abhängigkeit tatsächlich benötigter Extensionen.

5.3.5 Navigation und Flattening

Der Flatten-Operator flatten wird für die verschiedenen Varianten der Collections in der in Abschnitt 3.3.6, Band 1 beschriebenen Form realisiert und bei dem Flachdrücken von Navigationsergebnissen angewandt.

Die Navigation der OCL ist mengenwertig. Da Java eine Navigation ausgehend von mengen- und listenwertigen Strukturen nicht kennt, sind diese entsprechend zu transformieren. Tabelle 5.21 zeigt eine effiziente Umsetzung der Navigation.



Navigation: Mengen- und listenwertige Navigation


Erklärung

Navigation ausgehend von Containerstrukturen ist in Java durch Iteratoren zu realisieren.


einfache Navigation

a.role
   

a.role

 

  • Ausdruck a beschreibt ein einzelnes Objekt. Die Navigation entlang der gewünschten Assoziation ist durch role möglich.

  • Eine weitere Umsetzung durch Regeln für Assoziationen ist zu beachten. Zum Beispiel kann eine Kapselung von a durch Zugriffsmethoden vorgenommen werden.

  • Die Navigation kann ein mengenwertiges Ergebnis haben oder qualifiziert sein. Dann ist gegebenenfalls eine Anpassung der Container-Form notwendig.


Navigation aus einer Menge

sa.role
   

       Java/P         
Set<Class> res = new HashSet<Class>;
for(Iterator<Class2> it = sa’.iterator();
                          it.hasNext(); ) {
  Class2 a = it.next();
  if(a.role != null)
    res.add(a.role);
}
// Weiterverwendung von res

 

  • Ausdruck sa beschreibt eine Menge von Objekten. Die Navigation entlang der gewünschten Assoziation ist durch role möglich. Die Kardinalität ist „1“ oder „0..1“.

  • Siehe auch Aussagen zur einfachen Navigation.

  • Class ist der Typ der durch die Assoziation erreichten Klasse.

  • Class2 ist der Typ der Ausgangsklasse.


Mengenwertige Navigation aus einer Menge

sa.role
   

       Java/P         
Set<Class> res = new HashSet<Class>;
for(Iterator<Class2> it = sa’.iterator();
                          it.hasNext(); ) {
  Class2 a = it.next();
  res.addAll(a.role);
}
// Weiterverwendung von res

 

  • Im Unterschied zum obigen Fall ist die Kardinalität nun „“, also a.role mengenwertig.

  • Ansonsten gelten Aussagen der vorherigen Transformation.



Tabelle 5.21.: Navigation: Mengen- und listenwertige Navigation

5.3.6 Quantoren und Spezialoperatoren

Unendliche Quantoren, also Quantoren über den Grunddatentypen wie int und über Containerstrukturen wie List<Auction> werden nicht übersetzt. Alle objektwertigen Quantoren jedoch sind endlich und damit in Java übersetzbar. Eine entsprechende Realisierung in Form eines Iterators ist bereits im Eingangsbeispiel dieses Abschnitts gezeigt worden. Operatoren wie any oder iterate können in ähnlicher Form umgesetzt werden. Auch die Umsetzung von Referenzen auf Objektdiagramme in der OCL sind einfach. Ein Objektdiagramm wirkt als Prädikat, kann daher selbst in eine boolesche Methode transformiert und der Aufruf in den generierten Code eingesetzt werden. Das von der OCL/P zur Verfügung gestellte typeif-Konstrukt zur typsicheren Konversion von Objekten wird durch eine Abfrage mit instanceof und eine Typkonversion realisiert.

5.3.7 Methodenspezifikation

Die folgende Methodenspezifikation beschreibt für einen eingeschränkten Bereich, wie nach Eingang eines neuen Gebots eine neue Zeit für das Auktionsende festgelegt wird. Dabei sind einige zeitliche Abhängigkeiten zu sichern:

       OCL  
 context Time ConstantTimingPolicy.newCurrentClosingTime(
           Auction a, Bid b)
let   long old   = a.closingTime.timeSec;
      long now   = b.time.timeSec
pre:  status==RUNNING && isInExtension && now <= old
post: result.timeSec ==
           min(now + extensionTimeSecs,a.finishTime.timeSec)

Die Vorbedingung wirkt wie ein zu Beginn der Methode formuliertes ocl-Statement und die mit dem in der OCL zur Verfügung stehenden let-Konstrukt definierten Variablen werden als lokale Variable verstanden. Die Übersetzung dieser Spezifikation in erweitertes Java erfolgt in Abbildung 5.22 unter der Annahme, dass der eigentliche Methodenrumpf gegeben ist.

       Java/P         
class ConstantTimingPolicy {
  public Time newCurrentClosingTime(Auction a, Bid b) {
    let long old   = a.closingTime.timeSec;
    let long now   = b.time.timeSec;
    let boolean precond = (status==RUNNING && isInExtension
                                           && now <= old);
 
    // Rumpf der Java Funktion (ohne return-Anweisung)
    Time result = // Ausdruck der Return-Anweisung
    ocl !precond || (result.timeSec
         == OCL.min(now + extensionTimeSecs,
                    a.finishTime.timeSec));
    return result;
  }
}
Abbildung 5.22: OCL transformiert in erweitertes Java

Diese Methode aus Abbildung 5.22 kann, wie in in Anhang B, Band 1 beschrieben, in normales Java übersetzt werden. Gemäß der in Abschnitt 3.4.3, Band 1 beschriebenen Bedeutung von Methodenspezifikationen muss die Nachbedingung nur erfüllt sein, wenn die Vorbedingung gilt. Deshalb wird die Vorbedingung evaluiert und ihr Ergebnis in der Variable precond gespeichert, um in der Nachbedingung überprüft zu werden. Mit dieser Form der Transformation ist es möglich, mehrere Methodenspezifikationen, die unabhängig voneinander definiert oder geerbt wurden, gleichzeitig zu testen. Eine wie in Abschnitt 3.4.3, Band 1 beschriebene Integration ist daher für diesen Zweck nicht notwendig.

Je nach Generatoreinstellung werden ocl- und let-Anweisungen nicht übersetzt, im Fehlerfall eine Exception erzeugt, eine Warnung im Protokoll vermerkt und nach Bedarf ein Auszug des aktuellen Objekts ausgegeben. Das let-Konstrukt zur Definition von Hilfsvariablen in Java, die nicht in den Produktionscode gehören, wird wie erwartet durch lokale Variablendefinitionen realisiert. Es entsteht der in Abbildung 5.23 dargestellte Code, bei dem allerdings vereinfachend das Abfangen von Exceptions weggelassen wurde.

       Java   
   
 class ConstantTimingPolicy {
  public Time newCurrentClosingTime(Auction a, Bid b) {
    long old   = a.closingTime.timeSec;
    long now   = b.time.timeSec;
    boolean precond =
            (status==RUNNING && isInExtension && now <= old);
    // Rumpf der Java Funktion (ohne return-Anweisung)
    Time result = // Ausdruck der Return-Anweisung
    if(precond && !(result.timeSec
         == OCL.min(now + extensionTimeSecs,
                    a.finishTime.timeSec))
            alert(...);
    return result;
  }
}
Abbildung 5.23: OCL transformiert in Java

Dieser relativ einfachen Umsetzung steht eine substantielle Komplexität der Umsetzung von Methodenspezifikationen gegenüber, wenn die Nachbedingung auf die Ursprungswerte der Variablen zu Beginn des Methodenaufrufs zugreift. Für die Übersetzung solcher Zugriffe ist bei der Generierung eine Infrastruktur notwendig, die feststellt, welche früheren Werte gegebenenfalls benötigt werden und diese geeignet zwischenspeichert. Dabei können wie nachfolgend beschrieben deutliche Aufwände entstehen. Dazu wird das folgende aus Abschnitt 3.4.3, Band 1 adaptierte Beispiel betrachtet, das den Effekt eines Unternehmenswechsels einer Person unter der Annahme, dass das Unternehmen bereits erfasst ist, beschreibt:

       OCL  
 context Person.changeCompany(String name)
pre:  exists Company co: co.name == name
post: company.name          == name &&
      company.employees     == company.employees@pre     +1 &&
      company@pre.employees == company@pre.employees@pre -1

In der Nachbedingung wird unter anderem mit company@pre auf das vorherige Unternehmen der wechselnden Person und mit company.employees@pre auf die Anzahl der vorherigen Mitarbeiter des neuen Unternehmens zugegriffen. Der Ausdruck company@pre ist für den Codegenerator leicht zu identifizieren und sein Inhalt zu speichern. Dies wirkt, als ob dabei eine interne Hilfsvariable angelegt wird. Gleiches gilt für den Ausdruck company@pre.employees@pre:

       OCL  
 context Person.changeCompany(String name)
let   Company companyPre = company;
      int   employeesPre = company.employees
pre:  exists Company co: co.name == name
post: company.name          == name &&
      company.employees     == company.employees@pre +1 &&
      companyPre.employees  == employeesPre -1

Schwierigkeiten bereitet jedoch company.employees@pre, weil hier auf den früheren Zustand des neuen Unternehmens zugegriffen wird. Für einen Codegenerator ist es faktisch unmöglich Code zu generieren, der zu Beginn einer Methodenausführung errät, welches das neue company-Objekt sein wird und dann dessen alten Zustand speichert. Dieses Problem wird noch deutlicher am Ausdruck ad.auction@pre[id] sichtbar, der in der Klasse AllData zur Selektion einer Auktion mit dem Identifikator id verwendet werden kann. Da id erst nach Methodenausführung evaluiert wird, ist unklar, welche Auktion des Ursprungszustands von ad.auction selektiert werden wird. Deshalb gibt es drei Strategien damit umzugehen:

  1. Es werden die alten Zustände aller company-Objekte gespeichert. Dies erfordert allerdings bereits für kleine Testdatenstrukturen einen deutlichen Effizienzverlust und ist im Testeinsatz des Produktionssystems mit großen Datenbeständen nicht umzusetzen.
  2. Für Tests wird eine Infrastruktur generiert, die eine interne Protokollierung aller Attributänderungen einschließlich der ursprünglichen Werte durchführt. Dies erfordert ebenfalls eine geeignete Infrastruktur, hat aber unter Umständen den Vorteil, dass die Änderungshistorie eines gescheiterten Tests analysiert werden kann.
  3. Der Codegenerator warnt vor der Ineffizienz der Spezifikation oder weist sie sogar zurück mit dem Hinweis, eine effizientere Formulierung zu finden. Der Entwickler hat meist eine gute Vorstellung, welche Objektzustände tatsächlich zu speichern sind und legt diese explizit in let-Anweisungen ab.

Wird eine Effizienzsteigerung durch manuelle Umsetzung gewünscht, so kann im Beispiel etwa aus der Vorbedingung „erraten“ werden, welches company-Objekt relevant ist und damit die Vorbedingung vereinfacht werden kann:

       OCL  
 context Person.changeCompany(String name)
let   Company newCo = AllData.ad.company[name]
pre:  newCo != null
post: company.name          == name &&
      company.employees     == newCo.employees@pre +1 &&
      company@pre.employees == company@pre.employees@pre -1

Um sicherzugehen, dass das neue Unternehmen tatsächlich mit dem in der let-Anweisung definierten Unternehmen übereinstimmt, kann zusätzlich company==newCo in die Nachbedingung aufgenommen werden. Diese Form benötigt nun nur minimalen zusätzlichen Speicherplatz, wird allerdings bereits so detailliert und implementierungsnah, dass dann eine direkte Implementierung unter Umständen vorzuziehen ist.

5.3.8 Vererbung von Methodenspezifikationen

Ob eine Methodenspezifikation für die Implementierungen der Unterklassen gilt, wird gemäß Abschnitt 3.4.3, Band 1 durch den Stereotyp not-inherited bestimmt. Im Allgemeinen sind deshalb die geerbten Vor- oder Nachbedingungen zusätzlich zu testen. Für eine redundanzfreie Implementierung werden die zu prüfenden Bedingungen aber nicht direkt als Zusicherungen formuliert, sondern in eigenständige Methoden ausgelagert, die in Unterklassen in geeigneter Form zur Verfügung stehen. Dieses Verfahren wird „Percolation“ genannt [Bin99] und führt zu der in Abbildung 5.24 dargestellten Implementierung.

       Java   
   
 class ConstantTimingPolicy {
  // Vorbedingung ohne/mit vorbelegten Parametern
  boolean preNewCurrentClosingTime(Auction a, Bid b) {
    long old   = a.closingTime.timeSec;
    long now   = b.time.timeSec;
    return preNewCurrentClosingTime(a,b,old,now);
  }
  boolean preNewCurrentClosingTime(Auction a, Bid b,
                                        long old, long now){
    return status==RUNNING && isInExtension && now <= old;
  }
 
  // Nachbedingung
  boolean postNewCurrentClosingTime(Time result,
                     Auction a, Bid b, long old, long now) {
    return result.timeSec
         == OCL.min(now +
                    extensionTimeSecs,a.finishTime.timeSec);
  }
 
  // Eigentliche Funktion
  public Time newCurrentClosingTime(Auction a, Bid b) {
    long old   = a.closingTime.timeSec;
    long now   = b.time.timeSec;
    boolean precond = preNewCurrentClosingTime(a,b,old,now);
    // Rumpf der Java Funktion (ohne return-Anweisung)
    Time result = // Ausdruck der Return-Anweisung
    if(precond &&
             !postNewCurrentClosingTime(result,a,b,old,now))
            alert(...);
    return result;
  }
}
Abbildung 5.24: OCL-Bedingungen transformiert in Java-Prädikate

Ein Vorteil dieses Verfahrens ist, dass ein Testtreiber zusätzlich prüfen kann, ob die Vorbedingung der Methodenspezifikation auch erfüllt ist, indem er diese vorab auf den gegebenen Testdaten prüft. Dadurch wird verhindert, dass ein Test als Erfolg gewertet wird, weil die Testdaten fälschlicherweise so aufgebaut wurden, dass sie die Vorbedingung nicht erfüllen.

Das in Abbildung 5.24 dargestellte Verfahren kann analog für Invarianten verwendet werden, so dass auch diese in den Unterklassen für Tests zur Verfügung stehen.


Bernhard Rumpe. Agile Modellierung mit UML. Springer 2012