Code aus UML-Modellen über XMI generieren

(Auszug aus "XSLT Kochbuch" von Sal Mangano)

Problem

Sie möchten Code aus UML-Spezifikationen (Unified Modeling Language) generieren, sind aber unzufrieden mit den Ergebnissen, die der eingebaute Code-Generator Ihres UML-Werkzeugs produziert.

Das Entwurfsmuster »Zustand«

Ein endlicher Automat (engl. state machine) ist ein nützliches Design-Werkzeug, das ein Softwaresystem modelliert, dessen Verhalten sich mit der Zeit ändert, je nachdem, welche Ereignisse eintreten. Ein endlicher Automat wird oftmals als Graph dargestellt, in dem Knoten für bestimmte Zustände und Kanten (Verbindungen) für Zustandsübergänge als Antwort auf eine Eingabe oder ein Ereignis stehen.

Das Entwurfsmuster (engl. design pattern) »Zustand« bietet einen Ansatz zur Konstruktion eines endlichen Automaten, bei dem jeder konkrete Zustand von einer eigenen Klasse dargestellt wird, die von einer abstrakten Oberklasse namens State abgeleitet ist. Eine separate Klasse namens StateMachine stellt den endlichen Automaten als Ganzes dar und enthält einen Zeiger auf einen konkreten State, der den aktuellen Zustand darstellt. Die Methoden in der StateMachine werden an den aktuellen Zustand weitergeleitet, der ein zustandsspezifisches Verhalten implementiert. Beim Aufruf dieser Methoden kommt es eventuell dazu, dass die konkrete Zustandsklasse in ihrer übergeordneten StateMachine einen Übergang zu einem anderen Zustand bewirkt.

Lösung

XMI ist eine standardisierte XML-basierte Darstellung von UML, die von vielen UML-Modellierungswerkzeugen (z.B. Rational Rose) unterstützt wird. Auch wenn als Hauptzweck von XMI der Austausch von Modelldaten zwischen verschiedenen Werkzeugen angeführt wird, kann man es auch als Basis bei der Code-Generierung verwenden.

Die meisten UML-Werkzeuge unterstützen die Generierung von Code, erzeugen dabei aber selten mehr als ein Skelett des Objektmodells. Zum Beispiel verwenden alle mir bekannten UML-basierten Generatoren nur jene Information, die in den Klassendiagrammen vorhanden ist.

An dieser Stelle werden Sie Code für das Entwurfsmuster »Zustand«, engl. state, (siehe auch Entwurfsmuster: Elemente wiederverwendbarer objektorientierter Software von Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides (Addison-Wesley, 2001).) generieren, der Angaben aus einem Klassendiagramm mit Angaben aus einem Zustandsdiagramm kombiniert (siehe die beiden folgenden Abbildungen).

Klassendiagram zur Darstellung von Zuständen

Abbildung: Klassendiagram zur Darstellung von Zuständen.

Zustandsdiagramm für einen Anrufbeantworter

Abbildung: Zustandsdiagramm für einen Anrufbeantworter.

Der Code-Generator geht davon aus, dass der Designer folgende Konventionen beachtet hat:

  1. Die Zustandskontextklasse verfügt über das Stereotyp StateMachine.
  2. Die abstrakte Basisklasse der jeweiligen Zustandsklassen verfügt über das Stereotyp State.
  3. Alle Operationen im Zustandskontext, deren Implementierung an die Zustandsklassen weitergegeben wird, verfügen über das Stereotyp delegate (um anzugeben, dass sie ihre Zuständigkeit an den Zustand abgeben).
  4. Alle Operationen im Zustandskontext, die von den Zuständen für die Implementierung des Verhaltens des endlichen Automaten benutzt werden, verfügen über das Stereotyp action.
  5. Die einzige Ausnahme zu (4) ist eine bestimmte Operation, die aufgerufen werden sollte, wenn der Zustand nicht weiß, was er machen soll. Diese Operation hat das Stereotyp default und wird üblicherweise bei der Fehlerbehandlung benutzt.
  6. Die Zustände im UML-Zustandsdiagramm verwenden die gleichen Namen wie die konkreten Klassen, die vom State-Interface abgeleitet sind (2).
  7. Die mit den Übergängen verbundenen Aktionen und Wächtern (engl. actions und guards) beziehen sich auf den Kontext und verwenden die genauen Operationsnamen. Eine Aktion besteht aus dem Code, der bei einem Übergang ausgeführt wird, und ein Wächterausdruck ist eine Vorbedingung, die wahr sein muss, damit der Übergang stattfinden kann.

Das folgende Beispiel zeigt einen endlichen Automaten, der die grundlegende Funktionalität eines Anrufbeantworters implementiert, sobald Sie Ihre Nachrichten abrufen.

Das aus diesem einfachen Design generierte XMI kann hier wegen seines riesigen Umfangs nicht vollständig abgebildet werden. XMI verwendet horrend lange und anstrengende Namenskonventionen. Dieses Kapitel zeigt nur ein Fragment davon mit der Klasse AnsweringMachineState, um Ihnen einen Eindruck davon zu geben, und selbst dieses musste gekürzt werden:

<Foundation.Core.Class xmi.id="S.10011">
  <Foundation.Core.ModelElement.name>AnsweringMachineState</Foundation.Core.ModelElement.name>
  <Foundation.Core.ModelElement.stereotype>
    <Foundation.Extension_Mechanisms.Stereotype xmi.idref="G.22"/>
    <!--State -->
  </Foundation.Core.ModelElement.stereotype>
  <Foundation.Core.GeneralizableElement.specialization>
    <Foundation.Core.Generalization xmi.idref="G.91"/>
    <!-- {Connected->AnsweringMachineState}{3D6782A402EE} -->
    <Foundation.Core.Generalization xmi.idref="G.92"/>
    <!-- {HandlingMsg-&gt;AnsweringMachineState}{3D6782EF0119} -->
    <Foundation.Core.Generalization xmi.idref="G.93"/>
    <!-- {Start-&gt;AnsweringMachineState}{3D67B2FD004E} -->
    <Foundation.Core.Generalization xmi.idref="G.94"/>
    <!-- {GoodBye-&gt;AnsweringMachineState}{3D67B31B02AF} -->
  </Foundation.Core.GeneralizableElement.specialization>
  <Foundation.Core.Classifier.associationEnd>
    <Foundation.Core.AssociationEnd xmi.idref="G.25"/>
  </Foundation.Core.Classifier.associationEnd>
  <Foundation.Core.Classifier.feature>
    <Foundation.Core.Operation xmi.id="S.10012">
      <Foundation.Core.ModelElement.name>doCmd</Foundation.Core.ModelElement.name>
      <Foundation.Core.ModelElement.visibility xmi.value="public"/>
      <Foundation.Core.Feature.ownerScope xmi.value="instance"/>
      <Foundation.Core.BehavioralFeature.isQuery xmi.value="false"/>
      <Foundation.Core.Operation.specification/>
      <Foundation.Core.Operation.isPolymorphic xmi.value="false"/>
      <Foundation.Core.Operation.concurrency xmi.value="sequential"/>
      <Foundation.Core.ModelElement.stereotype>
        <Foundation.Extension_Mechanisms.Stereotype xmi.idref="G.23"/>
        <!-- pure -->
      </Foundation.Core.ModelElement.stereotype>
      <Foundation.Core.BehavioralFeature.parameter>
        <Foundation.Core.Parameter xmi.id="G.76">
          <Foundation.Core.ModelElement.name>cntx</Foundation.Core.ModelElement.name>
          <Foundation.Core.ModelElement.visibility xmi.value="private"/>
          <Foundation.Core.Parameter.defaultValue>
            <Foundation.Data_Types.Expression>
              <Foundation.Data_Types.Expression.language/>
              <Foundation.Data_Types.Expression.body/>
            </Foundation.Data_Types.Expression>
          </Foundation.Core.Parameter.defaultValue>
          <Foundation.Core.Parameter.kind xmi.value="inout"/>
          <Foundation.Core.Parameter.type>
            <Foundation.Core.Class xmi.idref="S.10001"/>
            <!-- AnsweringMachine -->
          </Foundation.Core.Parameter.type>
        </Foundation.Core.Parameter>
        <Foundation.Core.Parameter xmi.id="G.77">
          <Foundation.Core.ModelElement.name>cmd</Foundation.Core.ModelElement.name>
          <Foundation.Core.ModelElement.visibility xmi.value="private"/>
          <Foundation.Core.Parameter.defaultValue>
            <Foundation.Data_Types.Expression>
              <Foundation.Data_Types.Expression.language/>
              <Foundation.Data_Types.Expression.body/>
            </Foundation.Data_Types.Expression>
          </Foundation.Core.Parameter.defaultValue>
          <Foundation.Core.Parameter.kind xmi.value="inout"/>
          <Foundation.Core.Parameter.type>
            <Foundation.Core.DataType xmi.idref="G.65"/>
            <!-- Command -->
          </Foundation.Core.Parameter.type>
        </Foundation.Core.Parameter>
        <Foundation.Core.Parameter xmi.id="G.78">
          <Foundation.Core.ModelElement.name>doCmd.Return</Foundation.Core.ModelElement.name>
          <Foundation.Core.ModelElement.visibility xmi.value="private"/>
          <Foundation.Core.Parameter.defaultValue>
            <Foundation.Data_Types.Expression>
              <Foundation.Data_Types.Expression.language/>
              <Foundation.Data_Types.Expression.body/>
            </Foundation.Data_Types.Expression>
          </Foundation.Core.Parameter.defaultValue>
          <Foundation.Core.Parameter.kind xmi.value="return"/>
          <Foundation.Core.Parameter.type>
            <Foundation.Core.DataType xmi.idref="G.67"/>
            <!-- void -->
          </Foundation.Core.Parameter.type>
        </Foundation.Core.Parameter>
      </Foundation.Core.BehavioralFeature.parameter>
    </Foundation.Core.Operation>
  </Foundation.Core.Classifier.feature>
</Foundation.Core.Class>

Das folgende lange XSLT-Beispiel konvertiert das XMI in eine C++-Implementierung dieses endlichen Automaten, wobei es sich des Entwurfsmusters »Zustand« bedient. Aus purer Verzweiflung verwende ich hierbei XML-Entities, um lange XMI-Namen abzukürzen. Dieses Vorgehen kann ich nicht wirklich empfehlen, aber in diesem Fall war das die einzige Möglichkeit, das XSLT auf einer Seite vernünftig darzustellen. Außerdem reduziert das auch das inhärente von der XMI-DTD verursachte Hintergrundrauschen. Weiterhin werden Sie bemerken, dass Schlüssel, d.h. keys, ausgiebig verwendet werden, was daran liegt, dass XMI ausgiebigen Gebrauch von Querverweisen in Form von xmi.id- und xmi.idref-Attributen macht:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xslt [
  <!--=======================================================-->
  <!-- XMI-Organisationskonstrukte auf höherer Ebene         -->
  <!--=======================================================-->
  <!ENTITY BE "Behavioral_Elements">
  <!ENTITY SM "&BE;.State_Machines">
  <!ENTITY SMC "&SM;.StateMachine.context">
  <!ENTITY SMT "&SM;.StateMachine.top">
  <!ENTITY MM "Model_Management.Model">
  <!ENTITY FC "Foundation.Core">
  <!ENTITY FX "Foundation.Extension_Mechanisms">
  <!--=======================================================-->
  <!-- Abkürzungen für Grundelemente einer XMI-Datei, die    -->
  <!-- in diesem Stylesheet von besonderem Interesse sind.   -->
  <!--=======================================================-->
  <!-- Das Modell als Ganzes -->
  <!ENTITY MODEL "XMI/XMI.content/&MM;">
  <!-- Einige generische UML-Elemente -->
  <!ENTITY ELEM "&FC;.Namespace.ownedElement">
  <!-- Elemente von endlichen Automaten -->
  <!ENTITY STATEMACH "&MODEL;/&ELEM;/&SM;.StateMachine">
  <!ENTITY STATE "&SM;.CompositeState">
  <!ENTITY SUBSTATE "&STATE;.substate">
  <!ENTITY PSEUDOSTATE "&SM;.PseudoState">
  <!ENTITY PSEUDOSTATE2 "&SMT;/&STATE;/&SUBSTATE;/&PSEUDOSTATE;">
  <!ENTITY ACTION "&BE;.Common_Behavior.ActionSequence/&BE;.Common_Behavior.ActionSequence.action">
  <!ENTITY GUARD "&SM;.Transition.guard/&SM;.Guard/&SM;.Guard.expression">
  <!-- Die Assoziation als Ganzes -->
  <!ENTITY ASSOC "&FC;.Association">
  <!-- Der Verbindungsteil der Assoziation -->
  <!ENTITY CONN "&ASSOC;.connection">
  <!-- Die Enden einer Assoziation. -->
  <!ENTITY END "&ASSOC;End">
  <!ENTITY CONNEND "&CONN;/&END;">
  <!ENTITY ENDType "&END;.type">
  <!-- Eine UML-Klasse -->
  <!ENTITY CLASS "&FC;.Class">
  <!-- Der Name eines UML-Entity -->
  <!ENTITY NAME "&FC;.ModelElement.name">
  <!-- Eine Operation -->
  <!ENTITY OP "&FC;.Operation">
  <!-- Ein Parameter -->
  <!ENTITY PARAM "&FC;.Parameter">
  <!ENTITY PARAM2 "&FC;.BehavioralFeature.parameter/&PARAM;">
  <!-- Der Datentyp eines Parameters -->
  <!ENTITY PARAMTYPE "&PARAM;.type/&FC;.DataType">
  <!-- Ein UML-Stereotyp -->
  <!ENTITY STEREOTYPE "&FC;.ModelElement.stereotype/&FX;.Stereotype">
  <!-- Eine Supertyp-Relation (Vererbung) -->
  <!ENTITY SUPERTYPE "&FC;.Generalization.supertype">
  <!ENTITY GENERALIZATION "&FC;.GeneralizableElement.generalization/&FC;.Generalization">
  <!--=======================================================-->
  <!-- Formatierung                                          -->
  <!--=======================================================-->
  <!-- Wird für die Steuerung der Code-Einrückung benutzt -->
  <!ENTITY INDENT "    ">
  <!ENTITY INDENT2 "&INDENT;&INDENT;">
]>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>
  <!-- Indexklassen nach ihrer ID -->
  <xsl:key name="classKey" match="&CLASS;" use="@xmi.id"/>
  <xsl:key name="classKey" match="&CLASS;" use="&NAME;"/>
  <!-- Index-Stereoptypen nach Namen und xmi.id -->
  <xsl:key name="stereotypeKey" match="&FX;.Stereotype" use="@xmi.id"/>
  <xsl:key name="stereotypeKey" match="&FX;.Stereotype" use="&NAME;"/>
  <!-- Indexdatentypen nach ihrer ID -->
  <xsl:key name="dataTypeKey" match="&FC;.DataType" use="@xmi.id"/>
  <!-- Index-Assoziationen nach ihren Endklassen -->
  <xsl:key name="associationKey" match="&ASSOC;" use="&CONNEND;/&ENDType;/&FC;.Interface/@xmi.idref"/>
  <xsl:key name="associationKey" match="&ASSOC;" use="&CONNEND;/&ENDType;/&CLASS;/@xmi.idref"/>
  <!-- Indexgeneralisierungen nach ihrer ID -->
  <xsl:key name="generalizationKey" match="&FC;.Generalization" use="@xmi.id"/>
  <!-- Indexzustände und Pseudozustände zusammen nach ihrer ID -->
  <xsl:key name="stateKey" match="&SM;.SimpleState" use="@xmi.id"/>
  <xsl:key name="stateKey" match="&PSEUDOSTATE;" use="@xmi.id"/>
  <!-- Indexzustandsübergänge nach ihrer ID -->
  <xsl:key name="transKey" match="&SM;.Transition" use="@xmi.id"/>
  <!-- Indexübergangsereignisse nach ihrer ID -->
  <xsl:key name="eventsKey" match="&SM;.SignalEvent" use="@xmi.id"/>
  <!-- Die XMI-IDs von Stereotypen, mit denen das Entwurfsmuster Zustand in UML kodiert wird -->
  <xsl:variable name="STATE_MACH" select="key('stereotypeKey','StateMachine')/@xmi.id"/>
  <xsl:variable name="STATE" select="key('stereotypeKey','state')/@xmi.id"/>
  <xsl:variable name="DELEGATE" select="key('stereotypeKey','delegate')/@xmi.id"/>
  <xsl:variable name="ACTION" select="key('stereotypeKey','action')/@xmi.id"/>
  <xsl:variable name="DEFAULT" select="key('stereotypeKey','default')/@xmi.id"/>
  <xsl:variable name="PURE" select="key('stereotypeKey','pure')/@xmi.id"/>
  <!-- Wir benutzen Modi, um die Quellen in 5 Schritten zu generieren -->
  <xsl:template match="/">
    <!-- Deklariere Zustandsschnittstelle -->
    <xsl:apply-templates mode="stateInterfaceDecl" select="&MODEL;/&ELEM;/&CLASS;[&STEREOTYPE;/@xmi.idref = $STATE_MACH]"/>
    <!-- Deklariere konkrete Zustände -->
    <xsl:apply-templates mode="concreteStatesDecl" select="&MODEL;/&ELEM;/&CLASS;[not(&STEREOTYPE;)]"/>
    <!-- Deklariere Zustandskontextklasse -->
    <xsl:apply-templates mode="stateDecl" select="&MODEL;/&ELEM;/&CLASS;[&STEREOTYPE;/@xmi.idref = $STATE_MACH]"/>
    <!-- Implementiere Zustände -->
    <xsl:apply-templates mode="concreteStatesImpl" select="&MODEL;/&ELEM;"/>
    <!-- Implementiere Kontext -->
    <xsl:apply-templates mode="stateContextImpl" select="&MODEL;/&ELEM;/&CLASS;[&STEREOTYPE;/@xmi.idref = $STATE_MACH]"/>
  </xsl:template>
  <!-- ZUSTANDSKONTEXTDEKLARATION -->
  <xsl:template match="&CLASS;" mode="stateDecl">
    <!-- Finde die mit diesem Zustandskontext assoziierte Klasse, die eine Zustandsimplementierung ist. Das ist der Typ der aktuellen state-Member-Variablen des endlichen Automaten. -->
    <xsl:variable name="stateImplClass">
      <xsl:variable name="stateClassId" select="@xmi.id"/>
      <xsl:for-each select="key('associationKey',$stateClassId)">
        <xsl:variable name="assocClassId" select="&CONNEND;[&ENDType;/*/@xmi.idref != $stateClassId]/&ENDType;/*/@xmi.idref"/>
        <xsl:if test="key('classKey',$assocClassId)/&STEREOTYPE;/@xmi.idref = $STATE">
          <xsl:value-of select="key('classKey',$assocClassId)/&NAME;"/>
        </xsl:if>
      </xsl:for-each>
    </xsl:variable>
    <xsl:variable name="className" select="&NAME;"/>
    <xsl:text>&#xa;class </xsl:text>
    <xsl:value-of select="$className"/>
    <xsl:text>&#xa;{&#xa;public:&#xa;&#xa;</xsl:text>
    <xsl:text>&INDENT;</xsl:text>
    <xsl:value-of select="$className"/>::<xsl:value-of select="$className"/>
    <xsl:text>( );&#xa;&#xa;</xsl:text>
    <!-- Delegates sind Operationen, die an den aktuellen Zustand weiterleiten -->
    <xsl:apply-templates select="*/&OP;[&STEREOTYPE;/@xmi.idref = $DELEGATE]" mode="declare"/>
    <!-- void changeState(AbstractState& newState) -->
    <xsl:text>&INDENT;void changeState(</xsl:text>
    <xsl:value-of select="$stateImplClass"/>
    <xsl:text>& newSate) ;&#xa;</xsl:text>
    <xsl:text>&#xa;&#xa;</xsl:text>
    <!-- Nicht-Delegates sind andere Operationen, die Zustände im Kontext aufrufen -->
    <xsl:apply-templates select="*/&OP;[&STEREOTYPE;/@xmi.idref != $DELEGATE]" mode="declare"/>
    <xsl:text>&#xa;private:&#xa;&#xa;</xsl:text>
    <xsl:text>&INDENT;</xsl:text>
    <xsl:value-of select="$stateImplClass"/>
    <xsl:text>* m_State ;</xsl:text>
    <xsl:text>&#xa;&#xa;} ;&#xa;&#xa;</xsl:text>
  </xsl:template>
  <!-- DEKLARATION KONKRETER ZUSTÄNDE -->
  <xsl:template match="&CLASS;" mode="concreteStatesDecl">
    <xsl:text>class </xsl:text>
  <xsl:value-of select="&NAME;"/>
    <xsl:call-template name="baseClass"/>
    <xsl:text>&#xa;{&#xa;public:&#xa;&#xa;</xsl:text>
    <!-- Konkrete Zustände sind Singletons, d.h., wir generieren eine Instanzmethode -->
    <xsl:text>&INDENT;static </xsl:text>
    <xsl:value-of select="&NAME;"/>
    <xsl:text>&amp; instance( ) ;</xsl:text>
    <xsl:text>&#xa;&#xa;private:&#xa;&#xa;</xsl:text>
    <!-- Wir schützen die Konstruktoren von Singletons -->
    <xsl:text>&INDENT;</xsl:text>
    <xsl:value-of select="&NAME;"/>
    <xsl:text>( ) {  } &#xa;</xsl:text>
    <xsl:text>&amp;INDENT;</xsl:text>
    <xsl:value-of select="&NAME;"/>(const <xsl:value-of select="&NAME;"/>
    <xsl:text>&) {  } &#xa;</xsl:text>
    <xsl:text>&amp;INDENT;void operator =(const </xsl:text>
    <xsl:value-of select="&NAME;"/>
    <xsl:text>&) {  } &#xa;</xsl:text>
    <xsl:apply-templates select="*/&OP;" mode="declare"/>
    <xsl:text>} ;</xsl:text>
  </xsl:template>
  <!-- Template für die Deklaration von Klassen mit allen public Members -->
  <xsl:template match="&CLASS;" mode="declare">
    <xsl:text>class&#x20;</xsl:text>
    <xsl:value-of select="&NAME;"/>
    <xsl:text>&#xa;{&#xa;public:&#xa;&#xa;</xsl:text>
    <xsl:apply-templates select="*/&OP;" mode="declare"/>
    <xsl:text>&#xa;} ;&#xa;</xsl:text>
  </xsl:template>
  <xsl:template match="&OP;" mode="declare">
    <xsl:variable name="returnTypeId" select="&PARAM2;[&PARAM;.kind/@xmi.value = 'return']/&PARAMTYPE;/@xmi.idref"/>
    <xsl:text>&INDENT;</xsl:text>
    <xsl:if test="&STEREOTYPE;/@xmi.idref = $PURE">
      <xsl:text>virtual </xsl:text>
    </xsl:if>
    <xsl:value-of select="key('dataTypeKey',$returnTypeId)/&NAME;"/>
      <xsl:text>&#x20;</xsl:text>
    <xsl:value-of select="&NAME;"/>
    <xsl:text>(</xsl:text>
    <xsl:call-template name="parameters"/>
    <xsl:text>)</xsl:text>
    <xsl:if test="&STEREOTYPE;/@xmi.idref = $PURE">
      <xsl:text> = 0 </xsl:text>
    </xsl:if>
    <xsl:text>;&#xa;</xsl:text>
  </xsl:template>
  <!-- Extra Textknoten aufessen  -->
  <xsl:template match="text( )" mode="declare"/>
  <!-- DEKLARATION DER ZUSTANDSSCHNITTSTELLE -->
  <xsl:template match="&CLASS;" mode="stateInterfaceDecl">
    <xsl:text>// Vorwärtsdeklarationen&#xa;</xsl:text>
    <xsl:text>class </xsl:text>
    <xsl:value-of select="&NAME;"/>
    <xsl:text>;&#xa;&#xa;</xsl:text>
    <xsl:variable name="stateClassId" select="@xmi.id"/>
    <!-- Finde die mit dem Zustandskontext assoziierte Klasse, die ein Zustand ist. -->
    <xsl:for-each select="key('associationKey',$stateClassId)">
      <xsl:variable name="assocClassId" select="&CONNEND;[&ENDType;/*/@xmi.idref != $stateClassId]/&ENDType;/*/@xmi.idref"/>
      <xsl:if test="key('classKey',$assocClassId)/&STEREOTYPE;/@xmi.idref = $STATE">
        <xsl:apply-templates select="key('classKey',$assocClassId)" mode="declare"/>
      </xsl:if>
    </xsl:for-each>
    <xsl:text>&#xa;&#xa;</xsl:text>
  </xsl:template>
  <!-- Extra Textknoten aufessen -->
  <xsl:template match="text( )" mode="stateInterfaceDecl"/>
  <!-- IMPLEMENTIERUNG DES ZUSTANDSKONTEXTS -->
  <xsl:template match="&CLASS;" mode="stateContextImpl">
    <xsl:variable name="stateImplClass">
      <xsl:variable name="stateClassId" select="@xmi.id"/>
      <xsl:for-each select="key('associationKey',$stateClassId)">
        <xsl:variable name="assocClassId" select="&CONNEND;[&ENDType;/*/@xmi.idref != $stateClassId]/&ENDType;/*/@xmi.idref"/>
        <xsl:if test="key('classKey',$assocClassId)/&STEREOTYPE;/@xmi.idref = $STATE">
          <xsl:value-of select="key('classKey',$assocClassId)/&NAME;"/>
        </xsl:if>
      </xsl:for-each>
    </xsl:variable>
    <xsl:variable name="className" select="&NAME;"/>
    <xsl:text>// Konstruktor&#xa;</xsl:text>
    <xsl:value-of select="$className"/>::<xsl:value-of select="$className"/>
    <xsl:text>( )&#xa;</xsl:text>
    <xsl:text>{&#xa;</xsl:text>
    <xsl:text>&INDENT;// Endl. Automaten mit Startzustand initialisieren &#xa;</xsl:text>
    <xsl:variable name="startStateName">
      <xsl:call-template name="getStartState">
        <xsl:with-param name="classId" select="@xmi.id"/>
      </xsl:call-template>
    </xsl:variable>
    <xsl:text>&INDENT;m_State = &</xsl:text>
    <xsl:value-of select="$startStateName"/>
    <xsl:text>::instance( ) ;&#xa;</xsl:text>
    <xsl:text>}&#xa;&#xa;</xsl:text>
    <!-- void changeState(AbstractState& newState) -->
    <xsl:text>void </xsl:text>
    <xsl:value-of select="$className"/>
    <xsl:text>::changeState(</xsl:text>
    <xsl:value-of select="$stateImplClass"/>
    <xsl:text>&amp; newState)&#xa;</xsl:text>
    <xsl:text>{&#xa;</xsl:text>
    <xsl:text>&INDENT;m_State = &amp;newState;</xsl:text>
    <xsl:text>&#xa;}&#xa;&#xa;</xsl:text>
    <xsl:for-each select="*/&OP;[&STEREOTYPE;/@xmi.idref = $DELEGATE]">
      <xsl:variable name="returnTypeId" select="&PARAM2;[&PARAM;.kind/@xmi.value = 'return']/&PARAMTYPE;/@xmi.idref"/>
      <xsl:value-of select="key('dataTypeKey',$returnTypeId)/&NAME;"/>
      <xsl:text>&#x20;</xsl:text>
      <xsl:value-of select="$className"/>::<xsl:value-of select="&NAME;"/>
      <xsl:text>(</xsl:text>
      <xsl:call-template name="parameters"/>
      <xsl:text>)&#xa;</xsl:text>
      <xsl:text>{&#xa;</xsl:text>
      <xsl:text>&INDENT;m_State-></xsl:text>
      <xsl:value-of select="&NAME;"/>
      <xsl:text>(*this, </xsl:text>
      <xsl:for-each select="&PARAM2;[&PARAM;.kind/@xmi.value != 'return']">
        <xsl:value-of select="&NAME;"/>
        <xsl:if test="position( ) != last( )">
          <xsl:text>, </xsl:text>
        </xsl:if>
      </xsl:for-each>
      <xsl:text>);&#xa;</xsl:text>
      <xsl:text>}&#xa;&#xa;</xsl:text>
    </xsl:for-each>
    <xsl:for-each select="*/&OP;[&STEREOTYPE;/@xmi.idref != $DELEGATE]">
      <xsl:variable name="returnTypeId" select="&PARAM2;[&PARAM;.kind/@xmi.value = 'return']/&PARAMTYPE;/@xmi.idref"/>
      <xsl:value-of select="key('dataTypeKey',$returnTypeId)/&NAME;"/>
      <xsl:text>&#x20;</xsl:text>
      <xsl:value-of select="$className"/>::<xsl:value-of select="&NAME;"/>
      <xsl:text>(</xsl:text>
      <xsl:call-template name="parameters"/>
      <xsl:text>)&#xa;</xsl:text>
      <xsl:text>{&#xa;</xsl:text>
      <xsl:text>&INDENT;// TODO: Verhalten dieser Aktion implementieren&#xa;</xsl:text>
      <xsl:text>}&#xa;&#xa;</xsl:text>
    </xsl:for-each>
  </xsl:template>
  <xsl:template match="text( )" mode="stateContextImpl"/>
  <!-- IMPLEMENTIERUNG KONKRETER ZUSTÄNDE -->
  <xsl:template match="&CLASS;" mode="concreteStatesImpl">
    <xsl:variable name="classId" select="@xmi.id"/>
    <xsl:variable name="stateMachine" select="/&STATEMACH;[&SMC;/&CLASS;/@xmi.idref = $classId]"/>
    <!-- Generiere die Implementierung aller Zustände im Automaten -->
    <xsl:for-each select="$stateMachine//&PSEUDOSTATE; | $stateMachine//&SM;.SimpleState">
      <xsl:call-template name="generateState">
        <xsl:with-param name="stateClass" select="key('classKey',&NAME;)"/>
      </xsl:call-template>
    </xsl:for-each>
  </xsl:template>
  <!-- Extra Textknoten aufessen -->
  <xsl:template match="text( )" mode="concreteStatesImpl"/>
  <xsl:template name="generateState">
    <!-- Das ist eine XMI-Klasse, die dem Zustand entspricht -->
    <xsl:param name="stateClass"/>
    <!-- Der aktuelle Kontext ist irgendein Zustand -->
    <xsl:variable name="state" select="."/>
    <xsl:variable name="className" select="&NAME;"/>
    <xsl:if test="$className != $stateClass/&NAME;">
      <xsl:message terminate="yes">State and class do not match!</xsl:message>
    </xsl:if>
    <xsl:for-each select="$stateClass/*/&OP;">
      <xsl:variable name="returnTypeId" select="&PARAM2;[&PARAM;.kind/@xmi.value = 'return']/&PARAMTYPE;/@xmi.idref"/>
      <xsl:value-of select="key('dataTypeKey',$returnTypeId)/&NAME;"/>
      <xsl:text>&#x20;</xsl:text>
      <xsl:value-of select="$className"/>::<xsl:value-of select="&NAME;"/>
      <xsl:text>(</xsl:text>
      <xsl:call-template name="parameters"/>
      <xsl:text>)&#xa;</xsl:text>
      <xsl:text>{&#xa;</xsl:text>
      <xsl:for-each select="$state/&SM;.StateVertex.outgoing/&SM;.Transition">
        <xsl:call-template name="generateStateBody">
          <xsl:with-param name="transition" select="key('transKey',@xmi.idref)"/>
        </xsl:call-template>
      </xsl:for-each>
      <xsl:text>&INDENT2;context.errorMsg( ) ;&#xa;</xsl:text>
      <xsl:text>}&#xa;&#xa;</xsl:text>
    </xsl:for-each>
  </xsl:template>
  <xsl:template name="generateStateBody">
    <xsl:param name="transition"/>
    <xsl:text>&INDENT;if (cmd == </xsl:text>
    <xsl:variable name="eventId" select="$transition/&SM;.Transition.trigger/&SM;.SignalEvent/@xmi.idref"/>
    <xsl:value-of select="key('eventsKey',$eventId)/&NAME;"/>
    <xsl:if test="$transition/&SM;.Transition.guard">
      <xsl:text> &amp;&amp; </xsl:text>
      <xsl:value-of select="$transition/&GUARD;/*/Foundation.Data_Types.Expression.body"/>
      <xsl:text>( )</xsl:text>
    </xsl:if>
    <xsl:text>)&#xa;</xsl:text>
    <xsl:text>&INDENT;{&#xa;</xsl:text>
    <xsl:text>&INDENT2;</xsl:text>
    <xsl:value-of select="$transition/&SM;.Transition.effect/&ACTION;/*/&NAME;"/>
    <xsl:text>( ) ;&#xa;</xsl:text>
    <xsl:variable name="targetStateId" select="$transition/&SM;.Transition.target/*/@xmi.idref"/>
    <xsl:if test="$targetStateId != $transition/@xmi.id"/>
      <xsl:text>&INDENT2;cntx.changeState(</xsl:text>
      <xsl:value-of select="key('stateKey',$targetStateId)/&NAME;"/>
      <xsl:text>::instance( ));&#xa;</xsl:text>
    <xsl:text>&INDENT;}&#xa;</xsl:text>
    <xsl:text>&INDENT;else&#xa;</xsl:text>
  </xsl:template>
  <!-- Generiere Funktionsparameter -->
  <xsl:template name="parameters">
    <xsl:for-each select="&PARAM2;[&PARAM;.kind/@xmi.value != 'return']">
      <xsl:choose>
        <xsl:when test="&PARAMTYPE;">
          <xsl:value-of select="key('dataTypeKey',&PARAMTYPE;/@xmi.idref)/&NAME;"/>
        </xsl:when>
        <xsl:when test="&PARAM;.type/&CLASS;">
          <xsl:value-of select="key('classKey', &PARAM;.type/&CLASS;/@xmi.idref)/&NAME;"/>
          <xsl:text>&amp;</xsl:text>
        </xsl:when>
      </xsl:choose>
      <xsl:text>&#x20;</xsl:text>
      <xsl:value-of select="&NAME;"/>
      <xsl:if test="position( ) != last( )">
        <xsl:text>, </xsl:text>
      </xsl:if>
    </xsl:for-each>
  </xsl:template>
  <!-- Generiere Basisklassen -->
  <xsl:template name="baseClass">
    <xsl:if test="&GENERALIZATION;">
      <xsl:text> : </xsl:text>
      <xsl:for-each select="&GENERALIZATION;">
        <xsl:variable name="genAssoc" select="key('generalizationKey',@xmi.idref)"/>
        <xsl:text>public </xsl:text>
        <xsl:value-of select="key('classKey', $genAssoc/&SUPERTYPE;/&CLASS;/@xmi.idref)/&NAME;"/>
        <xsl:if test="position( ) != last( )">
          <xsl:text>, </xsl:text>
        </xsl:if>
      </xsl:for-each>
    </xsl:if>
  </xsl:template>
  <xsl:template name="getStartState">
    <xsl:param name="classId"/>
    <xsl:variable name="stateMachine" select="/&STATEMACH;[&SMC;/&CLASS;/@xmi.idref = $classId]"/>
    <xsl:value-of select="$stateMachine/&PSEUDOSTATE2; [&PSEUDOSTATE;.kind/@xmi.value = 'initial']/&NAME;"/>
  </xsl:template>
</xsl:stylesheet>

Hier ist ein Teil des resultierenden C++-Codes:

// Vorwärtsdeklarationen
class AnsweringMachine;

class AnsweringMachineState
{
public:

    virtual void doCmd(AnsweringMachine& cntx, Command cmd) = 0 ;

} ;


class Connected : public AnsweringMachineState
{
public:

    static Connected& instance( ) ;

private:

    Connected( ) {  }
    Connected(const Connected&) {  }
    void operator =(const Connected&) {  }
    void doCmd(AnsweringMachine& cntx, Command cmd);

} ;

class HandlingMsg : public AnsweringMachineState
{
public:

    static HandlingMsg& instance( ) ;

private:

    HandlingMsg( ) {  }
    HandlingMsg(const HandlingMsg&) {  }
    void operator =(const HandlingMsg&) {  }
    void doCmd(AnsweringMachine& cntx, Command cmd);

} ;

// WEITERE ZUSTÄNDE WURDEN AUS PLATZGRÜNDEN WEGGELASSEN...

class AnsweringMachine
{
public:

    AnsweringMachine::AnsweringMachine( );

    void doCmd(Command cmd);
    void changeState(AnsweringMachineState& newSate) ;


    bool moreMsgs( );
    void playRemaining( );
    void playCurrentMsg( );
    void advanceAndPlayMsg( );
    void deleteMsg( );
    void sayGoodBye( );
    void errorMsg( );

private:

    AnsweringMachineState* m_State ;
   } ;

void Connected::doCmd(AnsweringMachine& cntx, Command cmd)
{
    if (cmd =  = NEXT_MSG && cntx.moreMsgs( ))
    {
        cntx.playCurrentMsg( ) ;
        cntx.changeState(HandlingMsg::instance( ));
    }
    else
    if (cmd =  = NEXT_MSG && !cntx.moreMsgs( ))
    {
        cntx.playRemaining( ) ;
        cntx.changeState(Connected::instance( ));
    }
    else
    if (cmd =  = END)
    {
        cntx.sayGoodBye( ) ;
        cntx.changeState(GoodBye::instance( ));
    }
    else
        context.errorMsg( );
}
// WEITERE ZUSTÄNDE WURDEN AUS PLATZGRÜNDEN WEGGELASSEN...

// Konstruktor
AnsweringMachine::AnsweringMachine( )
{
    // Endl. Automaten mit Startzustand initialisieren
    m_State = &Start::instance( ) ;
}

void AnsweringMachine::changeState(AnsweringMachineState& newState)
{
    m_State = &newState;
}

void AnsweringMachine::doCmd(Command cmd)
{
    m_State->doCmd(*this, cmd);
}

bool AnsweringMachine::moreMsgs( )
{
    // TODO: Verhalten dieser Aktion implementieren
}

void AnsweringMachine::playRemaining( )
{
    // TODO: Verhalten dieser Aktion implementieren
}

void AnsweringMachine::playCurrentMsg( )
{
    // TODO: Verhalten dieser Aktion implementieren
}

// WEITERE AKTIONEN WURDEN AUS PLATZGRÜNDEN WEGGELASSEN...

Diskussion

Es gibt drei Gründe dafür, warum Sie vielleicht einen eigenen UML-Code-Generator schreiben möchten. Erstens könnte Ihnen eventuell der Stil des Codes oder der Code selbst nicht gefallen, den der eingebaute Code-Generator des Modellierungswerkzeugs erzeugt. Zweitens generiert das Werkzeug vielleicht keinen Code in der Sprache, die Sie benötigen. Zum Beispiel kenne ich kein UML-Werkzeug, das Python-Code generieren würde. (Nicht, dass ich ein großer Fan dieser klammerlosen Sprache wäre, aber sie begeistert eine riesige Menge von Entwicklern.) Und drittens möchten Sie vielleicht die Generierung eines höheren Entwurfsmusters oder eines Software-Dienstes automatisieren, der sehr wichtig bei Ihrer Code-Entwicklung ist. Letzteres ist auch die Motivation hinter dem Beispiel dieses Lösungsabschnitts.

Das Erstellen allgemeiner Code-Generierungsmöglichkeiten für mehrere Zielsprachen auf Basis von XMI und XSLT ist ein interessantes Projekt, geht aber weit über den Rahmen eines einelnen Beispiels hinaus. Die Lösung dieses Beispiels ist nicht als Generator für den Einsatz in der Produktion gedacht, sondern ein Anfang dafür. Zu den möglichen Verbesserungen gehören insbesondere die folgenden vier:

  • Erstens sollte die Generierung von XSLT in kleine Komponenten zerlegt werden, die einzelne Teile verschiedener Konstrukte der Zielsprache generieren. Das ähnelt dem, was wir unter XML in SVG umwandeln bei der Generierung von SVG gemacht haben.
  • Zweitens sollte der generierende XSLT-Code mehr UML-Informationen benutzen können, z.B. Zugriffsrechte, anstatt diese Angaben fest zu kodieren.
  • Drittens sollte der Generator XSLT 1.0-Erweiterungen oder XSLT 2.0 verwenden, um Code in mehreren Header- und Quelldateien statt in einer monolithischen Datei generieren zu können.
  • Viertens könnten beim Layout von Code mehrere Stilarten unterstützt werden, wenngleich das nicht ganz so wichtig ist. Zum Beispiel folgt der C++-Code hier dem Allman-Stil, aber viele C++-Programmierer bevorzugen (törichterweise?) den K&R-Stil. Andererseits können Sie den generierten Code auch einfach einem separaten Schönheitsprogramm übergeben und alle Formatierungsfragen völlig ignorieren.

  

zum Seitenanfang

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema XSLT bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © 2006 O'Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "XSLT Kochbuch" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

O'Reilly Verlag GmbH & Co. KG, Balthasarstraße 81, 50670 Köln, kommentar(at)oreilly.de