Agile Modellierung mit
UML
Loading

5.1 Übersetzung von Klassendiagrammen

In diesem Abschnitt wird die Transformation von Konzepten des Klassendiagramms und einiger damit zusammenhängender Elemente in Java als komplexeres Beispiel durch eine Sammlung von Transformationsregeln beschrieben, die auch die Möglichkeiten zur Beschreibung von Alternativen und von Kompositionen der Regeln zeigen. Bei dieser Übersetzung werden weder Java-Frameworks oder Infrastrukturkonzepte wie JavaBeans oder Middleware-Komponenten noch Datenbank-Anbindungen berücksichtigt. Dafür sind jeweils spezielle Generatoren notwendig, die in der hier angestrebten allgemeinen Form nicht diskutiert werden können.

5.1.1 Attribute

Für normale und statische Attribute von Klassen wurde mit der Transformationsregel von Abbildung 4.12 bereits eine Regel zur Umsetzung angegeben. Diese kann im Prinzip auch für abgeleitete Attribute verwendet werden, ignoriert jedoch ein wesentliches Merkmal dieser Art von Attributen. Im Normalfall existiert für ein abgeleitetes Attribut eine als Invariante formulierte Berechnungsvorschrift. Alternativ dazu kann auch eine bereits in der Zielsprache Java formulierte Methode existieren, die die Berechnung des Attributs vornimmt. Nach unserer Konvention heißt eine solche Methode calcAttr.



Attribut2eager: Abgeleitete Attribute - Eager Version


Erklärung

Für ein abgeleitetes Attribut /attr existiert eine Berechnungsvorschrift als OCL-Invariante der Form attr=expr oder eine Methode calcAttr.

Änderungen der zur Berechnung verwendeten Attribute treten gegenüber der Abfrage des abgeleiteten Attributs selten auf. Deshalb wird das abgeleitete Attribut sofort neu berechnet und gespeichert.


Attributdefinition

 
   

       Java   
   
 class Class { ...
  private Type attr;
  tags’ synchronized Type getAttr() {
      return attr;
  }
  private synchronized void calcAttr() {
      attr = expr’;
  }
}

 

  • Attributdefinition und getAttr werden wie bei der Standardregel Attribut1 transformiert. Eine setAttr Methode existiert jedoch nicht.

  • Ist die Berechnungsvorschrift als nicht-rekursive OCL-Bedingung in der Form attr=expr angegeben, so wird diese in Java-Code expr' transformiert und in die Methode calcAttr eingebettet. Alternativ kann diese Methode bereits existieren oder durch eine Nachbedingung in der angegebenen Form spezifiziert sein.


Attributzugriff

attr
   

getAttr()
quali.attr
   

quali.getAttr()
  • Wie in Transformationsregel Attribut1.


Attributbesetzung

ist nicht möglich.


Besetzung eines Ausgangsattributs

durch Analyse des OCL-Ausdrucks beziehungsweise der vorhandenen calcAttr-Implementierung können die Ausgangsattribute ermittelt werden, von denen attr abgeleitet ist. Für jedes Ausgangsattribut source wird die set-Methode erweitert:

 

   

       Java   
   
  
class Class { ...
  tags’ synchronized Type’ setSource(Type’ a) {
      source=a;
      calcAttr();
      return a;
  }
}
  • Dies zeigt nur den (einfachen) Fall, dass Ausgangs- und abgeleitetes Attribut in demselben Objekt lokalisiert sind. Ist das nicht der Fall, muss eine bidirektionale Verbindung zwischen beiden Objekten bestehen, die durch die aktuellen Veränderungen nicht beeinträchtigt sein sollte.1


Beachtenswert

Die Veränderung eines Attributs hat die automatische Veränderung aller davon abgeleiteten Attribute zur Folge. Dies kann zu kaskadenartigen Neuberechnungen abgeleiteter Attribute führen und ineffizient sein, wenn mehr Änderungen als Abfragen auftreten.

Zirkuläre Abhängigkeiten führen darüber hinaus zu nichtterminierenden Neuberechnungen und sind daher verboten.



Tabelle 5.1.: Attribut2eager: Abgeleitete Attribute - Eager Version

Der oben formulierten „eager“ Version der Umsetzung kann eine „lazy“ Version entgegengesetzt werden, die den Attributwert nur bei Bedarf berechnet. Aufgrund der Ähnlichkeiten zur vorherigen Transformationsregel wird diese verkürzt wiedergegeben:



Attribut2lazy: Abgeleitete Attribute - Lazy Version


Erklärung

Für ein abgeleitetes Attribut /attr existiert eine Berechnungsvorschrift als OCL-Invariante der Form attr=expr oder eine Methode calcAttr.

Die Häufigkeit der Änderungen der zur Berechnung verwendeten Attribute liegt in einer ähnlichen Größenordnung wie die Abfrage des abgeleiteten Attributs. Deshalb wird das abgeleitete Attribut erst bei Bedarf berechnet und nicht gespeichert.


Attributdefinition

 
   

       Java   
   
 class Class { ...
  tags’ synchronized Type getAttr() {
      return calcAttr();
  }
  private synchronized Type calcAttr() {
      return expr’;
  }
}

 

  • Siehe Anmerkungen zu Attribut2eager.


Attribute

  • Attributzugriff erfolgt wie bei Attribut2eager.

  • Die direkte Attributbesetzung ist wie bei Attribut2eager nicht möglich.

  • Die Besetzung eines Ausgangsattributs (von dem dieses abhängt) muss nicht angepasst werden.


Beachtenswert

Vorteil gegenüber der Attribut2eager Version ist, dass der Kontrollfluss nicht invertiert wurde und damit keine bidirektionale Assoziationen oder eine andere Infrastruktur notwendig sind. Ineffizienz kann aber durch wiederholt durchgeführte Berechnung des Attributs entstehen.

Zirkuläre Abhängigkeiten führen auch hier zu nichtterminierenden Neuberechnungen und sind daher verboten.



Tabelle 5.2.: Attribut2lazy: Abgeleitete Attribute - Lazy Version

Eine im Bereich der graphischen Oberflächen gelegentlich verwendete Form des Model-View-Controller-Pattern nutzt Vorteile beider Ansätze, indem die change propagation nur in einer booleschen Statusvariable vermerkt wird, aber eine Neuberechnung erst bei Bedarf erfolgt.

5.1.2 Methoden

Für die Implementierung von Methoden stehen mehrere Strategien zur Verfügung, die von der Ausgangsituation abhängig sind:

  1. Der Methodenrumpf ist bereits in einem anderen Artefakt formuliert und muss nur in die Methode eingesetzt werden. Dabei werden auch die notwendigen Transformationen beispielsweise von Attributzugriffen vorgenommen.
  2. Die Methode ist durch ein Vor-/Nachbedingungspaar beschrieben, wobei die Nachbedingung, wie in Abschnitt 4.1 diskutiert, algorithmisch formuliert ist und direkt in Code umgesetzt werden kann.
  3. Die Methode ist durch ein Vor-/Nachbedingungspaar beschrieben, das aber nicht algorithmisch umsetzbar und deshalb nur für Tests geeignet ist.
  4. Für diese Methode gibt es noch keine Implementierung oder Spezifikation.

Für jeden dieser Fälle ist eine eigenständige Vorgehensweise notwendig. Der erste Fall benötigt nur die Integration des Methodenrumpfs mit der Signatur sowie die Umsetzung zum Beispiel der Attributzugriffe im Methodenrumpf.



Methode1impl: Methoden mit gegebener Implementierung


Erklärung

Eine Methode meth mit gegebenem Methodenrumpf code wird in Java umgesetzt.


Methodendefinition

 

  • Der Methodenrumpf code besteht aus einer Sequenz von Anweisungen. Diese wird entsprechend der gültigen Transformation für Anweisungen in code’ transformiert, um zum Beispiel Attributzugriff und -besetzung oder die Umsetzung von Zusicherungen (assertions) zu behandeln.

  • Sind Sichtbarkeitsangaben, Parameternamen, -typen und Ergebnistyp teilweise im Text und im Diagramm gegeben, so dürfen sie sich ergänzen, aber nicht widersprechen.



Tabelle 5.3.: Methode1impl: Methoden mit gegebener Implementierung

Für die Beschaffung des Methodenrumpfs gibt es in den heute verfügbaren Werkzeugen mehrere Ansätze. Eine Möglichkeit ist, den Rumpf als Textstück, zum Beispiel als Kommentar, der Methodensignatur im Diagramm zu hinterlegen und durch Anwählen zugänglich zu machen. Dies ist allerdings für große Systeme mit vielen Methoden nicht praktikabel. Die Technik des „Round Trip Engineering“ liest die Methodenrümpfe direkt aus dem Quellcode, um sie dorthin zurück zu schreiben.2

Neben oder statt Java-Implementierungen können auch OCL-Spezifikationen von Methoden in der Form von Vor- und Nachbedingungspaaren verwendet werden. In Abschnitt 3.4.3, Band 1 ist die Integration mehrerer solcher Methodenspezifikationen behandelt worden. Deshalb kann hier von einem einzelnen Paar ausgegangen werden. Ist die Spezifikation algorithmisch in der in Abschnitt 4.1.2 diskutierten Form, so kann daraus direkt Code erzeugt werden.3

Weil die Umsetzung einer derartig spezifizierten Methode im Wesentlichen auf der in Abschnitt 5.3 diskutierten Umsetzung von OCL in Java-Code beruht, soll hier auf eine explizite Formulierung der Transformationsregel verzichtet werden.

Im dritten oben genannten Fall existiert sowohl eine Implementierung als auch eine Spezifikation. Damit ist es sinnvoll, die Spezifikation zur Prüfung während der Laufzeit einzusetzen. Der Generator weiß, ob er effizienten Produktionscode oder mit diesen Prüfungen instrumentierten Code erzeugen soll. Zum Beispiel bieten Eiffel- und Java-Übersetzer die Möglichkeit, Zusicherungen optional zu übersetzen.

Im Prinzip ist nur die Vorbedingung vor Start der Methode und die Nachbedingung nach deren Ende zu testen. Dabei sind jedoch unter Umständen mit dem let-Konstrukt lokal definierte Variable und eventuell in der Nachbedingung genutzte Anfangszustände von Attributen zu sichern. Diese Sicherung kann komplex sein, wenn die benutzten Attribute in anderen Objekten liegen und die Zugangspfade ihrerseits verändert worden sein können. Eine über den Abschnitt 3.4.3, Band 1 hinausgehende ausführliche Diskussion dieser Problematik ist zum Beispiel in [RG02] zu finden.

Als letzte Variante soll hier noch der Fall kurz diskutiert werden, in dem es weder eine Implementierung noch eine algorithmisch ausführbare Spezifikation für eine Methode gibt. Dann kann die Methode nicht automatisiert implementiert werden. Für Simulationen und Tests, die diese Methode vielleicht nur marginal berühren, sind jedoch Strategien möglich und sinnvoll, Dummy-Implementierungen zu generieren.

  • Spielt die Methode bei den durchzuführenden Tests keine Rolle, so kann ein Fehleraufruf oder die Rückgabe eines Default-Werts in die Methode generiert werden.
  • Ist die Methode noch nicht realisiert, so kann ein interaktives Eingabefeld während Simulationsläufen dazu benutzt werden, dass der Nutzer auf Basis der aktuellen Parameter jeweils selbst das Ergebnis bestimmt.
  • Für eine, endliche Menge von Eingaben können in einer Tabelle Ergebnisse abgelegt sein. Diese Ergebnisse können zum Beispiel aus früheren interaktiven Simulationsläufen mitprotokolliert worden sein.

Einerseits ist eine interaktive Eingabe von Ergebnissen einzelner Methoden für automatisierte Testläufe nicht sinnvoll, andererseits können damit während der Vorführung eines Prototypen sofort Anwenderentscheidungen in das System zurückgeführt werden. Diese können protokolliert und später zum Beispiel als Testdaten genutzt werden. Diese interaktive Form des Erkenntnisgewinns ist sicherlich beschränkt, kann aber unter Umständen zu effektiverer Kommunikation mit Anwendern führen.

In der UML/P ist es nicht üblich, Hilfsmethoden wie getAttr in Klassendiagrammen explizit zu vermerken. Dadurch bleibt das Modell kompakter und übersichtlicher. Auch müssen diese Funktionen in Coderümpfen, die bei der Generierung übersetzt werden, nicht explizit verwendet werden. Es reicht aus, den Attributzugriff und die Attributbesetzung in Form von Zuweisungen einzusetzen. Ein Codegenerator übersetzt diese wie in den Transformationsregeln beschrieben in Methodenaufrufe. Es sollte jedoch erlaubt sein, diese Methoden direkt zu verwenden. Außerdem ist unter Umständen sinnvoll, die Generierung einer solchen Methode vorwegzunehmen, indem eine manuelle Implementierung angegeben wird. Dadurch lassen sich eventuell Optimierungen vornehmen oder zusätzliche Funktionalitäten realisieren.

5.1.3 Assoziationen

Eine unidirektionale Assoziation wird standardmäßig durch ein Attribut umgesetzt. Der Rollenname wird dabei als Attributname verwendet. Fehlt der notwendige Rollenname, so wird wie bei den in Abschnitt 3.3.8, Band 1 angegebenen Navigationsregeln ein Attributname aus dem Assoziationsnamen oder dem Namen der gegenüberliegenden Klasse gebildet.

Kardinalitäten werden entsprechend berücksichtigt: „0..1“ führt zu einem einfachen Attribut, das den Wert null annehmen darf, „1“ führt zu einem einfachen Attribut, das immer besetzt ist, und eine Assoziation mit Kardinalität „“ wird mengenwertig. Abhängig von zusätzlichen Merkmalen wie {ordered} stehen Mengen- oder Listen-Implementierungen zur Auswahl. Für qualifizierte Assoziationen wird entsprechend eine Abbildung (Map) zur Verfügung gestellt.

Bidirektionale Assoziationen werden durch Attribute auf beiden Seiten realisiert, die durch ein geeignetes Methodenprotokoll konsistent gehalten werden. Ist keine Navigationsrichtung angegeben, so wird eine geeignete Navigationsrichtung aus dem Kontext ermittelt und gegebenenfalls werden beide Richtungen realisiert.

Um die oben genannte Konsistenz bidirektionaler Assoziationen zu sichern, werden alle Zugriffe auf die Assoziation über generierte Methoden geführt. Die Form dieser generierten Methoden, also das für eine Assoziation verwendbare API, hängt von den Eigenschaften und Merkmalen der Assoziation ab.

So werden bei den Merkmalen {addOnly} und {frozen} entsprechende Funktionen zur Modifikation eingeschränkt. Abgeleitete Assoziationen werden mit denselben Prinzipien behandelt, wie abgeleitete Attribute. Das heißt, es werden nur Abfragemethoden zur Verfügung gestellt und diese durch Berechnungen implementiert.

Nachfolgende Transformation ist exemplarisch für bidirektionale, in beiden Richtungen mit Kardinalität „“ versehene Assoziationen.



Assoziation*,*,bidir: Bidirektionale Assoziation


Erklärung

Assoziationen werden in den Zustandsraum zumindest einer der beteiligten Klassen transformiert, indem entsprechende Attribute und Zugriffsfunktionen generiert werden.

Diese Transformationsregel ist für bidirektionale Assoziationen mit Kardinalität „“ in beiden Richtungen geeignet. Die Assoziation ist nicht abgeleitet und keine Komposition.


Definition der

Assoziation

  • Nachfolgende Ausführungen gelten für ClassB entsprechend, da die Situation symmetrisch ist.

  • Zugriffe auf die Assoziation werden durch Zugriffe auf das Attribut roleB modelliert, das die in Abschnitt 3.3.5, Band 1 eingeführte Signatur von Collection<ClassB> besitzt.

  • Der Attributname roleB extrahiert sich aus dem Rollennamen, dem Namen der Assoziation (assocname) oder wenn beide fehlen, dem Namen der gegenüberliegenden Klasse (classB). Allerdings muss die Eindeutigkeit des Namens gewährleistet sein (siehe Abschnitt 3.3.8, Band 1).

  • Die Umsetzung der zur Modellierung verwendeten Konstrukte in den Implementierungscode erfolgt relativ schematisch, jedoch werden verändernde Operationen wie addRoleB oder removeRoleB entsprechend angepasst, um damit die Konsistenz der bidirektionalen Assoziation sicherzustellen.


Zugriffsfunktionen

roleB.isEmpty()
   

roleB.isEmpty()
roleB.contains(obj)
   

roleB.contains(obj)

roleB.size
   

roleB.size()
roleB.iterator()
   

getIteratorRoleB()

etc.

  • Lesende Zugriffe bleiben weitgehend erhalten. Die Umsetzung entspricht der Standardumsetzung des OCL-Collection-Interface nach Java.

  • Über den Iterator können auch Links gelöscht werden (analog zu remove).


Modifikation

roleB.add(obj)
   

addRoleB(obj)
quali.roleB.add(obj)
   

quali.addRoleB(obj)

roleB.remove(obj)
   

removeRoleB(obj)
quali.roleB.remove(obj)
   

quali.removeRoleB(obj)

etc.

  • Modifizierende Zugriffe werden auf speziell generierte Methoden abgebildet.

  • Weitere modifizierende Zugriffe, wie zum Beispiel roleB.clear(), werden ebenfalls entsprechend abgebildet.


OCL-

Navigation

roleB
   

getRoleB()
quali.roleB
   

quali.getRoleB()
  • getRoleB liefert als Ergebnis eine unveränderbare Menge.4


Zusätzliche Methoden

   

       Java   
   
  
class ClassA { ...
  public synchronized Set<ClassB> getRoleB() {
      return Collections.unmodifiableSet(roleB);
  }
  public synchronized void addRoleB(ClassB b) {
      roleB.add(b);
      b.addLocalRoleA(this);
  }
  public synchronized void
                      addLocalRoleB(ClassB b) {
      roleB.add(b);
  }
}
  • Hilfsfunktionen wie addLocalRoleB oder removeLocalRoleB dürfen außerhalb dieses Protokolls nicht benutzt werden, obwohl sie als public generiert werden. Sie stehen deshalb dem Entwickler nicht zur Verfügung.5

  • Weitere Modifikatoren wie removeRoleB oder clearRoleB werden in ähnlicher Form generiert. Allerdings erfordert zum Beispiel clearRoleB in bidirektionalen Assoziationen die Abmeldung jedes Links auf der gegenüberliegenden Seite, hat also lineare Komplexität.

  • Ist das Merkmal {addOnly} angegeben, so stehen remove-Operationen nicht zur Verfügung.

  • Ist das Merkmal {ordered} angegeben, so wird eine Listen-Implementierung gewählt und die entsprechende Funktionalität zusätzlich angeboten.


Beachtenswert

Die durch ein Protokoll gesicherte Konsistenz zwischen beiden Enden einer bidirektionalen Assoziation besitzt im Normalfall nur konstanten Zusatzaufwand, ist also vertretbar. Ist eine Assoziation nur unidirektional, so kann dieser Aufwand dennoch wegfallen.



Tabelle 5.4.: Assoziation*,*,bidir: Bidirektionale Assoziation

Die Umsetzung von Assoziationen in Java-Code zeigt, wie groß die Variationsmöglichkeiten bei der Codegenerierung sind. Variabel abhängig von den Eigenschaften der Assoziation ist nicht nur das API einer Assoziation (also welche Funktionen in UML/P zum Zugriff und zur Manipulation zur Verfügung stehen), sondern auch die intern genutzte Datenstruktur. Da die Wahl der Datenstruktur zumindest Auswirkungen auf das Laufzeitverhalten der Implementierung hat, wird sinnvollerweise durch geeignete Steuerungsmechanismen wie etwa dem Merkmal {HashMap} oder durch geeignete Anpassung der Skripte die Auswahl der Implementierung ermöglicht.

Für Assoziationen mit beschränkten Kardinalitäten ist außerdem zu klären, wie der Versuch einer Verletzung der Kardinalität behandelt wird. Dafür gibt es zum Beispiel die Varianten, dies robust zuzulassen, aber gegebenenfalls eine Warnung zu protokollieren, bis hin zur Erzeugung einer Exception, die dann vom aufrufenden Objekt zu behandeln ist.

Neben der oben vorgeschlagenen Form der Implementierung einer Assoziation gibt es Vorschläge, die Links durch eigenständige Objekte zu realisieren oder durch eine global verwaltete Datenstruktur zu ersetzen. All diese Erweiterungen haben als Ziel, zusätzliche Funktionalität anzubieten, die durch das API der Modellierung zugänglich werden, oder Verhaltens- beziehungsweise Sicherheitseigenschaften zu optimieren. Eine globale statische Datenstruktur in Form einer Abbildung von Quell- zu Zielobjekt ist zum Beispiel von Interesse, wenn die Assoziation sehr dünn besetzt ist und der Speicherplatz dadurch effizienter genutzt wird. Dies sollte dem Nutzer der API verborgen bleiben, da es sich um Realisierungsdetails handelt.

Die notwendige Umsetzung von Java-Code zur Sicherung der Konsistenz der Assoziation zeigt, dass es wichtig ist, dass der Codegenerator die vollständige Kontrolle über alle Teile des generierten Codes, also auch über Methodenrümpfe hat. Dadurch wird beispielsweise die für bidirektionale Assoziationen gültige Konsistenzbedingung gesichert:

       OCL  
 context ClassA a, ClassB b inv:
 a.roleB.contains(b) <=> b.roleA.contains(a)

Verfahren des Roundtrip-Engineering können dies nicht leisten, da es dem Entwickler die Möglichkeit gibt, beliebig in generierte Datenstrukturen einzugreifen. Dort müsste also diese Konsistenzbedingung zur Laufzeit geprüft werden. Bei einer Transformation der Methodenrümpfe durch den Codegenerator können die Zugriffe und Modifikationen für die Assoziation überprüft beziehungsweise transformiert und damit verhindert werden, dass dem Entwickler die Methode addLocalRoleB zur Programmierung zur Verfügung steht.6

5.1.4 Qualifizierte Assoziation

Die qualifizierte Assoziation bietet gegenüber der normalen Assoziation ein angepasstes API, das die qualifizierte Selektion und Manipulation erlaubt, aber auch einige Operationen zur Modifikation unqualifizierter Assoziationen verbietet. Deshalb wird für die qualifizierte Assoziation eine eigene Transformationsliste angegeben, die auch das API beschreibt.



Assoziationquali: Qualifizierte Assoziation


Erklärung

Eine qualifizierte Assoziation wird ähnlich der normalen Assoziation umgesetzt, bietet aber angepasste Funktionalität für qualifizierten Zugriff.

Diese Transformationsregel ist geeignet für unidirektionale qualifizierte Assoziationen mit Kardinalität „1“.7 Die Assoziation ist weder abgeleitet noch eine Komposition.


Definiten der

Assoziation

  • Zugriffe auf die Assoziation werden durch Zugriffe auf das Attribut roleB modelliert, das eine Signatur der Form Map<QualiType,ClassB> besitzt.

  • Zusätzliche Methoden der unqualifizierten Assoziationen, wie das nachfolgend definierte addRoleB, sind möglich, weil der Qualifikator im Zielobjekt enthalten ist.


Zugriffsfunktionen

roleB.get(key)
   

roleB.get(key)
roleB.isEmpty
   

roleB.isEmpty()

 

roleB.containsValue(obj)
   

roleB.containsValue(obj)
roleB.keySet()
   

roleB.keySet()

 

roleB.containsKey(obj)
   

roleB.containsKey(obj)
roleB.values()
   

roleB.values()

roleB.size
   

roleB.size()
  • Lesende Zugriffe bleiben weitgehend erhalten.


Modifikation

roleB.clear()
   

roleB.clear()
roleB.put(key,obj)
   

putRoleB(key,obj)

 

roleB.removeValue(obj)
   

roleB.remove(obj.qualifier)
roleB.removeKey(obj)
   

roleB.remove(obj)

      

roleB.add(obj)
   

roleB.put(obj.qualifier,obj)
    etc.
  • Weitere modifizierende Zugriffe, wie zum Beispiel roleB.putAll, werden entsprechend abgebildet.

  • Die Methode remove(obj) für unqualifizierte Assoziationen wird für diese Form der qualifizierten Assoziationen nicht angeboten, weil eine gleichnamige Methode für Maps eine andere Funktionalität erfüllt (sie entfernt Schlüsselwerte). Stattdessen werden zwei Operationen mit jeweils eigenem Namen angeboten.

  • Bei gesetztem Merkmal {addOnly} stehen die remove-Operationen nicht zur Verfügung.


OCL-

Navigation

roleB
   

roleB.values()
roleB[key]
   

roleB.get(key)
  • Qualifizierte und unqualifizierte Navigation sind möglich.


Zusätzliche Methoden

   

       Java         
class ClassA { ...
  public synchronized Collection<ClassB>
                              getRoleB() {
    return Collections.unmodifiableCollection(
                          roleB.values());
  }
  public synchronized void putRoleB
                  (QualiType q, ClassB b) {
      if(q==b.qualifier) {
                    // Objekttypen nutzen equals()
        roleB.put(q,b);
      } else {
                    // Exception, Warnung oder
                    // robuste Implementierung
      }
  }
}

 

  • Die Methode putRoleB wird verwendet, um sicherzustellen, dass der Qualifikatorwert (Schlüssel) und der Wert des Attributs qualifier identisch sind.


Beachtenswert

Abhängig vom Zweck des Codes (Test, Simulation, Produktion) werden verschiedene Strategien für die Behandlung des Fehlerfalls von einer Fehlermeldung über eine Mitteilung in einem Protokoll bis hin zur robusten Implementierung eingesetzt.

Der Zugriff auf das hier verwendete Attribut roleB ist entsprechend der für dieses Attribut gültigen Transformation ebenfalls umzusetzen.



Tabelle 5.5.: Assoziationquali: Qualifizierte Assoziation

Die Transformation der qualifizierten Assoziation nutzt die Komponierbarkeit von Transformationsregeln, da hier zunächst eine Assoziation in ein Attribut transformiert wird, das durch eine weitere Transformation durch Zugriffsmethoden gekapselt wird. Bei dieser Kapselung durch Zugriffsmethoden ist allerdings zu beachten, dass die Methode getroleB zwei unterschiedliche Aufgaben zu erfüllen hat. Bei qualifizierten Assoziationen ist zwischen (1) der Menge aller durch die Links erreichbaren Objekte und dem (2) Attributinhalt zu unterscheiden. Nur bei normalen Assoziationen sind beide Bedeutungsvarianten identisch. Die Methode getroleB realisiert Variante (1). Für die Variante (2) wird bei Bedarf eine Methode mit dem Namen getroleBAttribute eingeführt, die hier ein Map-Objekt zurückgibt. Durch die zahlreichen qualifizierten Zugriffsmöglichkeiten sollte jedoch der Zugriff auf die realisierende Map-Datenstruktur durch den Modellierer nicht notwendig sein.

5.1.5 Komposition

Wie bereits in Abschnitt 2.3.4, Band 1 diskutiert, besteht zwischen den Lebenszyklen des Kompositums und den davon abhängigen Objekten eine zeitliche Beziehung. Diese ist jedoch durch erhebliche Interpretationsunterschiede gekennzeichnet. Die Komposition wird strukturell wie eine normale Assoziation behandelt, das Anlegen beziehungsweise Entfernen von Links aus einer Komposition unterliegt aber der jeweiligen Interpretation. Entsprechend werden einige Operationen des Assoziations-API nicht angeboten oder unterliegen Restriktionen.

Eine Interpretation des Kompositums, die relativ verbreitet ist und im Auktionsprojekt als einzige verwendet wurde, wird nachfolgend dargestellt.



Kompositionfrozen: Fixierte Komposition


Erklärung

Die fixierte Form der Komposition wird genutzt, wenn das abhängige Objekt dieselbe Lebensspanne wie das Kompositum hat, während der Initialisierungsphase des Kompositums erzeugt wird und der Link zwischen beiden Objekten unveränderbar ist.

Diese Transformationsregel ist geeignet für die unidirektionale Kompositionen mit Kardinalität „1“.


Kompositionsdefinition

 

  • Die Struktur entspricht einer Assoziation mit derselben Kardinalität.

  • Zugriffe auf die Assoziation werden durch Zugriffe auf das Attribut roleB modelliert, das einen einfachen Objekttyp hat.

  • Der Attributname roleB extrahiert sich aus dem Rollennamen, dem Namen der Assoziation (assocname) oder wenn beide fehlen (was bei Kompositionen häufig der Fall ist), dem Namen der gegenüberliegenden Klasse (classB). Allerdings muss die Eindeutigkeit des Namens gewährleistet sein (siehe Abschnitt 3.3.8, Band 1).


Zugriffsfunktion

roleB
   

roleB
  • Lesende Zugriffe auf das Attribut werden durch eine nachgeschaltete Transformationsregel (normalerweise in getRoleB()) umgesetzt.


Modifikation

Die Besetzung des Attributs roleB darf ausschließlich im Konstruktor, also der Initialisierungsphase erfolgen.8 Dafür wird entweder eine Factory oder ein new-Kommando eingesetzt:

 

  • Im Fall einer bidirektionalen Komposition wird in dem abhängigen Objekt durch einem in der Transformation Assoziation*,*,bidir beschriebenen Verfahren der entsprechende Link ebenfalls gesetzt. Dazu wird die oben gezeigte Besetzung mit roleB= durch einen Methodenaufruf setRoleB ersetzt.


OCL-

Navigation

wie in vorangegangenen Transformationsregeln


Beachtenswert

Die Restriktion, dass das abhängige Objekt erst im Konstruktor des Kompositums erzeugt wird, stellt sicher, dass abhängige Objekte nicht mehrfach verwendet werden.9 Eine weniger strikte Umsetzung würde zum Beispiel erlauben, das abhängige Objekt bereits als Parameter an den Konstruktor zu übergeben. Dann kann jedoch nicht mehr sicher festgestellt werden, ob das Objekt neu erzeugt wurde und damit der Kompositionsbeziehung genügt.



Tabelle 5.6.: Kompositionfrozen: Fixierte Komposition

5.1.6 Klassen

Die Übersetzung einer Klasse mit ihren Attributen, Methoden, Assoziationen, Kompositionen und den bislang noch nicht besprochenen Vererbungsbeziehungen ist relativ schematisch, da die kanonische Vorgehensweise die direkte Abbildung der UML-Klasse in die Java-Klasse ist. Die Umsetzung von Klassen ist jedoch stark getrieben durch Stereotypen und Merkmale, die steuern, welche zusätzliche Funktionalität und welche Varianten der Transformation von Attributen vorgenommen werden. In dieser Grundtransformation werden keine Stereotypen berücksichtigt.



Klassen: Umsetzung einer Klasse


Erklärung

Eine Klasse wird direkt übernommen. In Abhängigkeit der ihr beigefügten Stereotypen und Merkmale sowie genereller Übersetzungsvorgaben wird für die Klasse zusätzliche Funktionalität generiert, die dem Entwickler bei der Benutzung der Klasse zur Verfügung steht.


Klassendefinition

 

  • Vererbung und Interface-Implementierung werden übernommen.

  • Attribute, Assoziationen werden entsprechend der jeweils gültigen Regeln zu Code transformiert.

  • Stereotypen und Merkmale steuern sowohl die Umsetzung der genannten Modellierungselemente als auch die Generierung zusätzlicher Funktionalität.


Vergleichsfunktion

   

       Java         
class Class { ...
  public boolean equals(Object obj) {
    // Vergleich der Attribute
  }
}

 

  • Die Methode equals vergleicht die einzelnen, neu definierten Attribute und verwendet die gleichnamige Methode der Oberklasse.

  • Assoziationen und abgeleitete Attribute werden im Normalfall zum Vergleich nicht berücksichtigt, jedoch aber Kompositionen, bei denen die gerade bearbeitete Klasse das Kompositum darstellt.

  • Das Merkmal {Equals=Liste} erlaubt die explizite Auflistung, welche Attribute und Assoziationen in den Vergleich einbezogen werden. Abkürzend kann mit dem Merkmal Equals+ eine Liste zusätzlicher Assoziationen oder mit Equals- eine Negativliste auszunehmender Attribute spezifiziert werden.

  • Ist für die Klasse eine equals-Methode bereits explizit angegeben, so wird diese übernommen anstatt sie zu generieren.


Hashfunktion

   

       Java   
   
 class Class { ...
  public int hashCode() {
    // geeignete Berechnung aus den Attributen
  }
}

 

  • Die Hash-Funktion wird geeignet implementiert.

  • Mit den Merkmalen {Hash=Liste}, {Hash+} und {Hash-} kann analog zur Vergleichsfunktion gesteuert werden, welche Attribute dafür herangezogen werden.

  • Ist für die Klasse eine hash-Methode bereits explizit angegeben, so wird diese übernommen.


Stringumwandlung

   

       Java      
class Class { ...
  public String toString() {
    // Umsetzung der Attribute, Assoziationen
  }
}
  • Die Methode toString liefert eine einfache Umsetzung in einen String, der die Inhalte der beteiligten Attribute wiedergibt. Diese Form der Ausgabe dient vor allem für Tests und Simulationen und sollte im Produktionssystem normalerweise nicht eingesetzt werden.

  • Assoziationen, die im Zustandsraum der Klasse abgelegt sind, abgeleitete Attribute und Kompositionen werden miteinbezogen.

  • Die Merkmale {ToString=Liste}, {ToString+} und {ToString-} erlauben die Kontrolle darüber, welche Klassenelemente ausgegeben werden.

  • Das Merkmal {ToStringVerbosity=Nummer} erlaubt die Steuerung der Verbosität. 0: Keine Ausgabe, 1: Klassenname, 2: Attributinhalte sehr kompakt (ohne erreichbare und abhängige Objekte) und 6: verbose Ausgabe jedes Attributs und jeder Assoziation in der Form Attributname=Attributwert, die alle erreichbaren Objekte einschließt.

  • Ist für die Klasse eine toString-Methode bereits explizit angegeben, so wird diese übernommen.


Konstruktoren

   

       Java      
class Class { ...
  public Class() {
    // geeignete Besetzung der Attribute mit Defaults
  }
  public Class(Attributlist) {
    setAttribute(attribute); ...
  }
}

 

  • Konstruktoren werden gemäß der Generierungsstrategie erzeugt.

  • Wenn nicht explizit ausgeschlossen, dann ist standardmäßig der leere Konstruktor und ein Konstruktor zur Besetzung aller Attribute dabei.

  • Weil das Merkmal {new(Attributlist)} mehfrach anwendbar ist, können beliebig viele Konstruktoren erzeugt werden. Alternativ ist es auch möglich, Konstruktoren direkt anzugeben, weil so zusätzliche Funktionalität im Konstruktor realisiert werden kann.


Protokollausgabe

   

       Java         
class Class { ...
  public String stringForProtocol() {
    // Umsetzung der Attribute, Teile der Assoziationen
  }
}

 

  • Diese Methode arbeitet ähnlich wie toString, wird aber für die Ausgabe in Protokollen verwendet. Sie kann analog parametrisiert beziehungsweise manuell implementiert werden.


Beachtenswert

Neben stringForProtocol gibt es eine Reihe weiterer Funktionen, die in entsprechender Form realisiert werden, aber hier nicht erwähnt wurden. Einige sind aus der von allen Objekten abgeleiteten Klasse Object (beispielsweise clone), andere folgen aus Interfaces, die zu implementieren sind (beispielsweise compareTo aus dem Interface Comparable) und wieder andere sind bedingt durch Implementierungsvorgaben für den Codegenerator. Dazu gehören Funktionalitäten für die Protokollausgabe wie oben beschrieben, Speicherung, Fehlerbehandlung und zusätzliche Funktionen, die zur Bearbeitung von Tests hilfreich sind.



Tabelle 5.7.: Klassen: Umsetzung einer Klasse

Gerade für den Einsatz in Testumgebungen sind unter Umständen eine Reihe weiterer Methoden und Datenstrukturen für eine Klasse zu generieren. Bei der Generierung solcher uniformen Methoden für Implementierung und Tests kann ein Codegenerator wertvolle Dienste leisten.

Eine der wenigen und eher selten gewählten Alternativen zu der hier beschriebenen Abbildung sei dennoch erwähnt. Sie verzichtet darauf, das Typsystem der Zielsprache Java zu nutzen und legt stattdessen Attribute als Abbildung des Attributnamens auf den Wert mit dem HashMap (String, Object) ab. Es ist dann im Prinzip ausreichend, eine einzige Java-Klasse in der in Abbildung 5.8 dargestellten Form zu realisieren, die zwar einiges an zusätzlicher Flexibilität mit sich bringt, aber ineffizienter ist. Eine ähnliche Form wird zum Beispiel zur Ressourcen-Verwaltung von Parametern verwendet.

       Java   
   
 class Chameleon { ...
  // Träger aller Attribute
  HashMap(String,Object) attributes;
 
  public Object get(String attributeName) {
      return attributes.get(attributeName);
  }
 
  // Typprüfung: feststellen, ob bestimmte Attribute vorhanden sind
  public boolean isInstanceOf(Set<String> attributeNames) {
      return attributes.keySet().containsAll(attributeNames);
  }
}
Abbildung 5.8: Dynamische Verwaltung von Attributen

5.1.7 Objekterzeugung

Ein letzter interessanter Punkt im Kontext der Codeerzeugung für Klassen ist das Management ihrer Objekte. Dazu gehört beispielsweise die Erzeugung von Objekten, die Verwaltung und der effiziente Zugriff auf einzelne Objekte oder das Speichern und Laden von Datenbanken. Verwaltungstätigkeiten werden oft so genannten „Management-Objekten“ auferlegt, die neben einer Sammlung der im Speicher befindlichen Objekte die transaktionsgesteuerte Abbildung auf die Datenbank und den effizienten Zugriff geladener Objekte erlauben. Von all diesen Tätigkeiten soll nachfolgend nur die Objekterzeugung in Java diskutiert werden, da sie unter anderem für Tests instrumentierbar sein muss.

Die in den Coderümpfen verwendete Form des new Class(...) kann bei der Codeerzeugung durch den Aufruf geeigneter Factory-Methoden umgesetzt werden. Dies erhöht die Flexibilität bei der Codeerzeugung beträchtlich, da so Unterklassen verwendet oder in automatisierten Tests Dummies eingesetzt werden können.10



Objekterzeugung: Objekte mit einer Factory erzeugen


Erklärung

Im Quellcode wird die Objekterzeugung mit dem new-Konstrukt vorgenommen. Der generierte Code enthält stattdessen Factory-Aufrufe. Eine Standard-Factory wird generiert und kann durch Bildung von Unterklassen auf spezifische Situationen angepasst werden.


Objekterzeugung

new Class(Arguments)
   

Factory.newClass(Arguments)
  • Die generierte Klasse Factory besitzt eine statische Methode newClass die das neue Objekt erzeugt.

  • Das Attribut f wird bei der Systeminitialisierung standardmäßig belegt, darf aber überschrieben werden.

  • Überschreiben von createClass erlaubt Erzeugung von Objekten aus Subklassen, Singletons, Management von Objektmengen und mehr.


Klasse Factory

   

       Java      
public class Factory { ...
  public static initFactory() {
    f = new Factory();
  }
 
  // für jede Klasse Class
  public static Class newClass(Arguments) {
    return f.createClass(Arguments);
  }
 
  // erlaubt Überschreiben obiger statischen Methode
  protected static Factory f;
 
  protected Class createClass(Arguments) {
    return new Class(Arguments);
  }
}

 

  • Entsprechende Factory-Methoden werden für jede Klasse des Systems generiert.

  • Mehrere Factory-Methoden für dieselbe Klasse mit unterschiedlichen Parametersätzen werden erzeugt, wenn es entsprechende Konstruktoren gibt.

  • Eine Aufteilung der Factory in mehrere Klassen, zum Beispiel entsprechend einer Subsystem-Struktur, kann vorgenommen werden, muss dann aber vom Generator-Skript gesteuert werden.


Alternative

Es ist unter anderem möglich, statt einem einzelnen Attribut f für mehrere Gruppen von zu erzeugenden Klassen beziehungsweise sogar für jede Klasse ein eigenes Attribut einzusetzen, so dass die Generierung von Objekten individuell angepasst werden kann:

 

       Java         
public class Factory { ...
  public static initFactory() {
    fClass = new Factory(); ... // für jede Klasse
  }
 
  // für jede Klasse Class
  public static Class newClass(Arguments) {
    return fClass.createClass(Arguments);
  }
 
  protected static Factory fClass; ... // für jede Klasse
 
  protected Class createClass(Arguments) {
    return new Class(Arguments);
  }
}

wobei Aufrufe wieder so transformiert werden:

new Class(Arguments)
   

Factory.newClass(Arguments)


Tabelle 5.9.: Objekterzeugung: Objekte mit einer Factory erzeugen

Mehrstufige Übersetzung

Die exemplarisch diskutierten Varianten zur Umsetzung von Klassendiagrammen zeigen die hohe Bandbreite an möglichen Generierungsformen. Wie bereits diskutiert folgt daraus, dass die Codegenerierung eine grosse Flexibilität benötigt, um die jeweils notwendigen Aufgaben zu erfüllen. Ein Weg, die Flexibilität zu steigern, ist die Möglichkeit, aus mehreren Templates oder Skripten auszuwählen. Darüber hinaus nutzen sich die Templates gegenseitig, indem zum Beispiel Assoziationen zunächst in Attribute transformiert und diese dann durch Zugriffsmethoden gekapselt werden. Die in diesem Abschnitt gezeigten Regeln zur Transformation von Konzepten der Klassendiagramme in Java-Code sind daher nicht unabhängig voneinander. Abbildung 5.10 zeigt die Abhängigkeiten der Transformationsregeln.


Abbildung 5.10: Abhängigkeiten der behandelten Transformationsregeln zur Codegenerierung aus Klassendiagrammen

Dabei sind nur die explizit definierten Regeln beschrieben, aber es sollte für ein geeignetes Framework weitere Transformationsregeln geben, die durch weitere Templates festgelegt werden. Die Auswahl der Alternativen ist manchmal durch den Kontext oder Eigenschaften des übersetzen Konzepts vorgegeben (wie hier zum Beispiel bei den Assoziationen) oder kann durch Einstellungen des Generators gesteuert werden (wie zum Beispiel bei den abgeleiteten Attributen).


Bernhard Rumpe. Agile Modellierung mit UML. Springer 2012