MDA in Kürze
Schon seit Jahren werden Softwaresysteme mithilfe von Modellierungssprachen (z.B. UML) beschrieben. Anfangs wurden diese Modelle nur zu Dokumentationszwecken verwendet, später dann gab es Werkzeuge, mit denen aus den Modellen Quellcode erzeugt werden konnte. Das Roundtrip Engineering ermöglicht es zusätzlich, veränderten Quellcode wieder zurück in das Modell zu überführen. Dieses Verfahren hat u.a. den Nachteil, dass das Modell mit technischen Informationen belastet wird und die Transformationsregeln relativ starr sind.Ein anderes hochaktuelles Thema ist die aspektorientierte Entwicklung, die es uns ermöglicht, sog. Aspekte (das sind Eigenschaften, die sich quer durch ein System ziehen, z.B. Logging) nur an einer Stelle zu definieren.
MDA verbindet das Beste dieser beiden Ansätze. Bei einer einfachen Systementwicklung nach MDA wird ein Platform Independent Model (PIM) mithilfe eines UML-Werkzeuges erstellt. Dieses Modell enthält, wie der Name schon sagt, keinerlei plattformspezifische Informationen. Vielmehr wird die Domäne der Software modelliert und von technischen Details abstrahiert. Hier zeigt sich auch, wie gut sich der modellgetriebenen Ansatz mit einer domänengetriebenen Softwareentwicklung [5] kombinieren lässt.
Um aus einem PIM-Quellcode zu erzeugen, werden beliebig viele Transformationsschritte durchgeführt. In welcher Form diese Transformationen durchgeführt werden, ist leider noch nicht standardisiert, sodass es hier in der Praxis verschiedene Ansätze gibt. Das Ergebnis der Transformation kann ein Platform Specific Model (PSM) sein, das z.B. ebenfalls in UML beschrieben ist und alle zur Erzeugung von lauffähigem Code benötigten Informationen enthält. Es ist aber auch vorgesehen, diesen Schritt zu überspringen und aus dem PIM direkt den Quellcode zu generieren. Der zweite Ansatz wird in der Praxis wesentlich häufiger verfolgt.
Einen Webshop generieren
Wir wollen einen einfachen Webshop erstellen. In diesem Shop können verschiedene Artikel durch Kunden bestellt werden. Die technische Architektur des Webshops wird durch eine Persistenzschicht (wir werden das Framework Hibernate [2] verwenden) und eine Webapplikationsschicht bestimmt. Im Folgenden wird gezeigt, wie aus einem UML-Modell (PIM), welches die Domäne der Applikation repräsentiert, eine Persistenzschicht generiert werden kann. Die Hibernate-Implementierung besteht aus einfachen Java-Klassen, Mapping-Dateien und dem Modell für die relationale Datenbank. Die Generierung des User Interface wird in diesem Artikel nicht gezeigt. Der hier besprochene Webshop steht in Form zweier Eclipse-Projekte auf der Webseite des Open ArchitectureWare-Projektes [3] zum Download zur Verfügung.Für unser Vorhaben werden ein UML-Werkzeug und ein MDA-Generator benötigt. Das UML-Modell wird mit Poseidon for UML [4] erstellt, da es alle benötigten Anforderungen erfüllt, und als Community Edition kostenlos zur Verfügung steht. Zur Generierung verwenden wir den Generator des Open ArchitectureWare-Projektes.
Open ArchitectureWare
Das Open ArchitectureWare-Projekt (ursprünglich ein Projekt von B+M Informatik) ist seit Herbst 2003 bei Sourceforge [OAW] registriert. Das zentrale Produkt dieses Open-Source-Projektes ist der in diesem Artikel besprochene Generator (Open Generator Framework). Zusätzlich werden zahlreiche praktische Werkzeuge (z.B. Ant Tasks [6]) und Beispielapplikationen angeboten. Ein Plug-in für die aktuelle Eclipse-Plattform hilft dem Entwickler bei der Arbeit mit dem Generator. Mit diesem Plug-in können Xpand Templates in einem komfortablen Editor erstellt werden. Der Editor unterstützt Syntax Highlighting, Code Completion und Template-Validierung. Außerdem bietet es einen Template-Browser, der die Aufrufpfade der verschiedenen Templates visualisiert. Eine sehr aktive Entwickler-Community sorgt dafür, dass Anwendern schnell geholfen wird und neue Versionen und Erweiterungen regelmäßig veröffentlicht werden.Erstellen des fachlichen Modells
Ein Kunde (Customer) kann in seinen Einkaufswagen (ShoppingCart) verschiedene Produkte (Product) legen. Wie viele Einheiten des jeweiligen Produkts im Einkaufswagen liegen, wird über die Klasse ProductQuantity bestimmt. Außerdem werden den Produkten verschiedene Kategorien (Category) zugeordnet.
Das UML-Diagramm aus Abpictureung 1 zeigt das PIM für die Domäne des Webshops. Aus diesem rein fachlichen Modell soll die Persistenzschicht generiert werden. Die Klassen sind dem Stereotyp entity zugeordnet, Attribute dem Stereotyp persistent. An den verschiedenen Stereotypen wird der Generator später erkennen, welche Transformationen auf die jeweiligen Elemente angewendet werden sollen. Mit den Stereotypen erweitert man den Metatyp eines UML-Elements (z.B. wäre Customer normalerweise vom Metatyp Class, in diesem Falle ist der Metatyp aber durch den Stereotyp entity erweitert worden).
Unser erweitertes UML-Metamodell besteht also aus den Stereotypen entity für Class-Elemente und persistent für Attribut-Elemente. Eine solche Erweiterung nennt man auch UML-Profil. Wie man sieht, enthält das Modell keinerlei Informationen über die verwendete Persistenztechnik. Im Folgenden werden wir dem Generator beibringen, hieraus eine entsprechende Hibernate-Implementierung zu generieren.
Dem Generator das Generieren beibringen
Der ArchitectureWare Generator liest das erstellte PIM in Form einer XMI-Datei ein. XMI (XML Metadata Interchange) [7] ist ein von der OMG standardisiertes XML-Format, welches zum Austausch und Serialisieren von Metadaten genutzt wird. Der Generator sucht für jedes UML-Element in dieser Datei eine entsprechende Java-Klasse, mit der er das Element instanziieren kann. Hierbei werden entsprechende Java-Klassen für die Standard-UML-Metatypen vom Generator zur Verfügung gestellt. Dazu gehören z.B. Class, Package, Attribute usw. Ist ein Element mit einem Stereotyp gekennzeichnet, so sucht der Generator in einer Mapping-Datei nach einer entsprechenden Implementierung, die der User zur Verfügung stellt. Falls kein Mapping für z.B. entity definiert ist, wird der Standard-Metatyp verwendet (in diesem Fall de.bmiag.genfw.meta.classifier.Class). Das Mapping wird sehr einfach in XML definiert:<MetaMap><Mapping><Map>entity</Map><To>meta.Entity</To></Mapping><Mapping><Map>persistent</Map><To>meta.Persistent</To></Mapping></MetaMap>
Die Metaklassen werden in Java implementiert und erben von der entsprechenden Standard-Metaklasse:
package meta;import de.bmiag.genfw.meta.classifier.Class;public class Entity extends Class {}package meta;import de.bmiag.genfw.meta.classifier.Attribute;public class Persistent extends Attribute {}
Nachdem der Generator das PIM instanziiert hat, beginnt der eigentliche Transformationsvorgang. Beim ArchitectureWare Generator werden hierzu Templates verwendet. Die Templates werden in einer sehr einfachen Sprache namens Xpand definiert und bestehen aus so genannten DEFINE-Blöcken (im Folgenden Define genannt). Jeder dieser Blöcke hat einen Namen und einen Typ, für den er aufgerufen werden kann (vergleichbar mit Methoden in Programmiersprachen oder besser noch Templates in XSL). Innerhalb der Defines können weitere Xpand-Anweisungen stehen. Außerdem können hier auch die Methoden des übergebenen Elements aufgerufen werden. Wir werden als Erstes die Klassen für unseren Webshop erzeugen. Mithilfe des folgenden Define wird der Generator für jede Instanz von Entity die entsprechende Java-Klasse mit ihren Attributen erzeugt:
«DEFINE javaBean FOR Entity»«FILE Name".java"»/*** «Name»* «Documentation» **/public class «Name» {«FOREACH Attribute AS attribute»public «attribute.Type» «attribute.Name»;«ENDFOREACH»}«ENDFILE»«ENDDEFINE»
Xpand-Anweisungen bestehen aus Schlüsselwörtern (DEFINE, FOR, FOREACH usw.) und Methodenaufrufen (Name, Documentation, Attribute ...). Sie können Strings enthalten (z.B. in «FILE Name".java"») und auch Typnamen (Entity). Terminiert wird eine Xpand-Anweisung durch die Zeichen « und ». Mit dem erstellten Template wird für jede Entity eine einfache Java-Klasse mit entsprechenden Attributen generiert. Die Klassen liegen leider alle im Default Package, was wir nun ändern wollen. Um die Templates nicht mit zu viel Logik zu verunstalten, erweitern wir den Metatyp Entity um die Methoden aus Listing 1.
Listing 1
package meta.persistence;import de.bmiag.genfw.meta.classifier.Class;public class Entity extends Class {public String fullPackageName() {String packageName = "";de.bmiag.genfw.meta.classifier.Package pack = this.Package();while (pack != null) {packageName = pack.Name() + "." + packageName;pack = pack.SuperPackage();}return packageName.substring(0, packageName.length() - 1);}public String fullPackagePath() {String pack = FullPackageName();return pack.replace('.', '/');}}
«DEFINE javaBean FOR Entity»«FILE fullPackagePath"/"Name".java"»package «fullPackageName»;/*** «Name»* «Documentation»*/public class «Name» {«EXPAND field FOREACH Attribute»}«ENDFILE»«ENDDEFINE»«DEFINE field FOR Attribute»public «Type» «Name»;«ENDDEFINE»
Listing 2
«DEFINE javaBean FOR Entity»«FILE fullPackagePath"/"Name".java"»package «fullPackageName»;/*** «Name»* «Documentation»*/public class «Name» {«EXPAND field FOREACH Attribute»«EXPAND field FOREACH AssociationEnd»«EXPAND getter FOREACH Attribute»«EXPAND getter FOREACH AssociationEnd»«EXPAND setter FOREACH Attribute»«EXPAND setter FOREACH AssociationEnd»}«ENDFILE»«ENDDEFINE»«REM ----------------- Attributes --------------------»«DEFINE field FOR Persistent»private «JavaType» «Name»;«ENDDEFINE»«DEFINE getter FOR Persistent»public «JavaType» get«Name»() {return «Name»;}«ENDDEFINE»«DEFINE setter FOR Persistent»public void set«Name»(«JavaType» «Name») {this.«Name» = «Name»;}«ENDDEFINE»«REM -- outputs the type for a given AssociationEnd --»«DEFINE type FOR AssociationEnd»«IF Opposite.MultiplicityMax == "1"»«Opposite.Class.fullQualifiedName»«ELSE»java.util.Set«ENDIF»«ENDDEFINE»«REM -------------- AssociationEnds ------------------»«DEFINE field FOR AssociationEnd»private «EXPAND type FOR This» «Opposite.Name»;«ENDDEFINE»«DEFINE setter FOR AssociationEnd»public void set«Opposite.Name»(«EXPAND type FOR This» «Opposite.Name») {this.«Opposite.Name» = «Opposite.Name»;}«ENDDEFINE»«DEFINE getter FOR AssociationEnd»public «EXPAND type FOR This» get«Opposite.Name»() {return this.«Opposite.Name»;}«ENDDEFINE»
Der Einstiegspunkt des Generators
Beim Aufruf des Generators muss angegeben werden, für welchen Metatyp der Transformationsvorgang gestartet werden soll. Für diesen Typ muss es ein Define mit dem Namen Root geben (vergleichbar mit der main()-Methode in Java). Der Generator ruft dann für jede Instanz des Metatyps dieses Define einmal auf. Gibt man z.B. de.bmiag.genfw.meta.classifier.Class als Einstiegspunkt an, sucht der Generator nach dem folgenden Define:«DEFINE Root FOR Class»
Hier könnte mit einer If-Abfrage eine Typprüfung vorgenommen werden, damit das Define nur für Instanzen von Entity aufgerufen wird:
«DEFINE Root FOR Class»«IF This INSTANCEOF Entity»«EXPAND javaBean FOR This»«ENDIF»«ENDDEFINE»
Eine andere Möglichkeit besteht darin, den von Xpand unterstützten Polymorphismus zu benutzen. Dazu schreiben wir ein zweites, leeres Define für Class, dass dann automatisch für alle Class-Instanzen aufgerufen wird, die nicht vom Typ Entity sind (Listing 3).
Listing 3
«DEFINE Root FOR Class»«EXPAND javaBean FOR This»«ENDDEFINE»«DEFINE javaBean FOR Class»«ENDDEFINE»«DEFINE javaBean FOR Entity»«FILE fullPackagePath"/"Name".java"»package «fullPackageName»;/*** «Name»* «Documentation»*/public class «Name» {«EXPAND field FOREACH Attribute»«EXPAND field FOREACH AssociationEnd»«EXPAND getter FOREACH Attribute»«EXPAND getter FOREACH AssociationEnd»«EXPAND setter FOREACH Attribute»«EXPAND setter FOREACH AssociationEnd»}«ENDFILE»«ENDDEFINE»
Geschützte Regionen
Die Klasse ShoppingCart soll eine zusätzliche Business-Methode bekommen, die den zu zahlenden Betrag zurückgibt. Wir wollen also die generierte Klasse ShoppingCart manuell um eine Methode erweitern. Damit die Methode bei der nächsten Transformation nicht überschrieben wird, benutzen wir eine so genannte Protected-Region. Die entsprechenden Xpand-Schlüsselwörter lauten PROTECT und ENDPROTECT und kennzeichnen einen Abschnitt, der bei erneuter Generierung nicht überschrieben wird (Listing 4).Listing 4
«DEFINE javaBean FOR Entity»«FILE fullPackagePath"/"Name".java"»package «fullPackageName»;/*** «Name»* «Documentation»*/public class «Name» {«EXPAND field FOREACH Attribute»«EXPAND field FOREACH AssociationEnd»«EXPAND getter FOREACH Attribute»«EXPAND getter FOREACH AssociationEnd»«EXPAND setter FOREACH Attribute»«EXPAND setter FOREACH AssociationEnd»«PROTECT CSTART "//" CEND "" ID Id"BusinessLogic"»//hier können weitere Methoden stehen«ENDPROTECT»}«ENDFILE»«ENDDEFINE»
Listing 5
//PROTECTED REGION ID(IDfd98z234345g98BusinessLogic) START/*** returns the sum of all product quantity's sums in this cart** @return*/public BigDecimal getSum() {BigDecimal sum = new BigDecimal("0.00");for (Iterator iter = this.getProductQuantities().iterator(); iter.hasNext();) {ProductQuantity quantity = (ProductQuantity) iter.next();sum = sum.add(quantity.getSum());}return sum;}//PROTECTED REGION END
Hibernate Mapping generieren
Hibernate ist ein Persistenz Framework, dass durch eine einfache Verwendung überzeugt. Es benötigt für jede Klasse, die in einer relationalen Datenbank persistent gemacht werden soll, ein so genanntes Mapping. Das in Listing 6 gezeigte Template erzeugt für jede Entity ein XML-Element <mapping/>. Innerhalb dieses Mappings werden nacheinander die Defines property, manyToOne und set aufgerufen, welche die entsprechenden Elemente für das Hibernate Mapping erzeugen. Das Ergebnis für Customer sehen Sie in Listing 7.Listing 6
«DEFINE mapping FOR Entity»«FILE "src/"FullPackagePath"/"Name".hbm.xml"»<?xml version="1.0" ?>«EXPAND License::xmlLicense»<!DOCTYPE hibernate-mapping PUBLIC"-//Hibernate/Hibernate Mapping DTD 2.0//EN""http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd"><hibernate-mapping package="«FullPackageName»"><class name="«Name»" table="«Name»"><id name="_ID" column="_ID" type="long"><generator class="identity"/></id>«EXPAND property FOREACH Attribute»«EXPAND manyToOne FOREACH AssociationEnd»«EXPAND set FOREACH AssociationEnd»</class></hibernate-mapping>«ENDFILE»«ENDDEFINE»«DEFINE property FOR Attribute»«ENDDEFINE»«DEFINE property FOR Persistent»<property name="«Name»" column="«Name»" type="«HibernateType»"/>«ENDDEFINE»«DEFINE manyToOne FOR AssociationEnd»«IF Opposite.MultiplicityMax == "1"»<many-to-one name="«Opposite.Name»" column="«Opposite.Name»ID"/>«ENDIF»«ENDDEFINE»«DEFINE set FOR AssociationEnd»«IF Opposite.MultiplicityMax == "-1"»<set name="«Opposite.Name»"><key column="«Name»ID"/><one-to-many class="«Opposite.Class.fullQualifiedName»"/></set>«ENDIF»«ENDDEFINE»
Listing 7
<hibernate-mapping package="net.sf.architectureware.webshop.shoppingcart"><class name="Customer" table="Customer" ><id name="_ID" column="_ID" type="long"><generator class="identity"/></id><property name="name" column="name" type="string"/><property name="street" column="street" type="string"/><property name="city" column="city" type="string"/><property name="email" column="email" type="string"/><property name="phone" column="phone" type="string"/><set name="shoppingCarts"><key column="customerID"/><one-to-many class="net.sf.architectureware.webshop.shoppingcart.ShoppingCart"/></set></class></hibernate-mapping>
Wie geht's weiter?
Es können nun alle wesentlichen Dateien für die Persistenzschicht des Webshops generiert werden. Soll das Modell nun z.B. um eine Entity erweitert werden, müssen wir diese nur zum UML-Modell hinzufügen. Die entsprechenden Dateien und Referenzen werden dann automatisch vom Generator erzeugt.Beim Webshop-Beispiel von [3] wird auch das User Interface generiert (in Form einer Struts-Implementierung), sodass ca. 80 Prozent der Quelltexte automatisch erstellt werden. Die beiliegende Readme-Datei erklärt in elf Schritten, was zu tun ist, um das erste Buch in den Warenkorb zu legen. Insgesamt dauert der Installationsvorgang nicht länger als zehn Minuten. Danach können Sie nach Belieben Erweiterungen und Änderungen durchführen und dabei erleben, wie einfach und produktiv die Entwicklung mit einem MDA-Generator ist.
Wozu das Ganze?
Mithilfe des ArchitectureWare Generator kann eine klare Trennung zwischen fachlicher Domäne, technischer Architektur und der Kodierung erreicht werden. So wird auf der Ebene des PIM zusammen mit dem Fachexperten modelliert, ohne von technischen Belangen abgelenkt zu werden. Durch die Transformationsregeln und das erweiterte Metamodell wird eine klare Architektur definiert. Diese wird vom Entwickler mit manueller Programmierung, auf die auch beim modellgetriebenen Ansatz nicht verzichtet werden kann, angereichert. Modellgetriebene Softwareentwicklung zwingt den Entwickler dabei dazu, sich an die definierten Architekturen zu halten. Dadurch bleibt das Softwareprojekt sehr übersichtlich und strukturiert. Außerdem ist das UML-Modell, welches heutzutage in vielen Projekten zur Dokumentation mit gepflegt wird (zumindest wird dies oft versucht ;-)), zu jeder Zeit aktuell.Weiterhin soll uns die generative Softwareentwicklung dabei helfen, nötige Redundanzen automatisch zu pflegen und zu generieren. Viele Architekturen benutzen Konfigurationsdateien und Abstraktionsschichten, die ohne generative Techniken kaum pflegbar sind. Ein MDA-Generator kann Entwicklern hier viel lästige Arbeit abnehmen. Der Vorteil der MDA gegenüber anderen generativen Techniken ist vor allem in der Unabhängigkeit des fachlichen Modells (PIM) zu finden. Ein Wechsel der Plattform von z.B. Hibernate zu JDO [8] könnte durch einfaches Austauschen der Transformationsregeln (Xpand Templates) erreicht werden.
Schlussbemerkung
In diesem Artikel wurde die viel diskutierte Model Driven Architecture einmal in der Praxis gezeigt. Wir konnten erkennen, dass dieser Ansatz sehr viel Potenzial hat und schon heute die Produktivität in vielen Projekten enorm steigern kann. Verbesserungswürdig erscheinen jedoch die heute verfügbaren MDA-Werkzeuge. Das gilt für leichtgewichtige Open-Source-Generatoren genauso wie für kommerzielle, voll integrierte Umgebungen. Einer der Hauptkritikpunkte dürfte die fehlende Standardisierung der Transformationssprache sein. Würde man hier einen Standard schaffen, könnten Anwendungsfamilien (das sind Transformationsbeschreibungen und die dazugehörigen UML-Profile) zwischen den Werkzeugen ausgetauscht werden. Hieraus könnte sich ein ganz neuer Markt entwickeln.Ich hoffe, dass bei all denjenigen, die bisher noch keine praktischen Erfahrungen zu diesem Thema sammeln konnten, das Interesse hierfür geweckt wurde. Vielleicht feiern Sie schon in Ihrem nächsten Projekt dank MDA ganz neue Erfolge.
Sven Efftinge ist bei der lohndirekt Aktiengesellschaft als Projektleiter und Softwareentwickler tätig. Der Schwerpunkt seiner Arbeit liegt auf J2EE-Architekturen und der Eclipe-Plattform. Er ist aktiv am Open ArchitectureWare-Projekt beteiligt.
Links und Literatur
[1] Model Driven Architecture: www.omg.org/mda/
[2] Hibernate: www.hibernate.org
[3] Open ArchitectureWare: www.architectureware.org
[4] Poseidon for UML: www.gentleware.com
[5] Eric Evans: Domain-Driven Development. Tackling Complexity in the Heart of Software, Addison-Wesley, 2003
[6] Ant: ant.apache.org
[7] XML Metadata Interchange: www.omg.org/technology/documents/formal/xmi.htm
[8] Java Data Objects: access1.sun.com/jdo/





