Motivation
Aktuell mache ich mir Gedanken, wie man Datenmodelle aus .NET-Code in UML darstellen kann – und zwar so, dass alle Informationen aus dem Code auch im UML-Modell abgebildet sind. Über Datenmodellierung habe ich vor kurzem schon einen Beitrag geschrieben: Datenmodelle in Code und Datenmodellierung mit UML.
Annotationen in Code
Die modernen objektorientierten Programmiersprachen kennen so genannte Annotationen. Dies sind Zusatzinformationen, die man einem Programmcodeelement hinzufügen kann. Beispielsweise kann man damit einer Eigenschaftsvariable zusätzliche Informationen für eine JSON- oder XML-Serialisierung mitgeben.
In der Sprache Java heißen diese Dinge „Annotationen“ und werden mit einem @-Zeichen begonnen, in C# heißen sie C#-Attribute und werden mit eckigen Klammern umfasst. Aufgrund der Verwechselungsgefahr mit UML-Attributen möchte ich hier auch bei C# allgemein weiter von Annotationen sprechen.
Im Java-Code (Abbildung 1) unten sieht man eine Annotation, die einer Methode zugeordnet ist.
|
1 2 3 4 |
@SuppressWarnings({"deprecation"}) public void eineMethode() { EineDeprecatedKlasse b = new EineDeprecatedKlasse(); } |
In C# sehen solche Dinge so aus – hier eine Datenmodell-Klasse für eine Notiz (Abbildung 2):
|
1 2 3 4 5 6 7 8 9 10 11 |
public class Note { [JsonProperty("guid")] public string GUID { get; set; } = Guid.NewGuid().ToString(); [JsonProperty("title")] public string Title { get; set; } = ""; [JsonProperty("description")] public string Description { get; set; } = ""; } |
Im Beispiel wird den Property-Variablen mit Hilfe der Annotationen ein anderer Name (hier in Kleinschreibung) für die JSON-Serialisierung zugeordnet.
Annotationen sind jeweils durch eine eigene Klasse definiert (die von System.Attribute erbt) und können dann in anderen Klassen verwendet werden. Die Verwendung entspricht dann einer Instanz, da hier den Properties der Annotation konkrete Werte zugewiesen werden.
Annotationen mit UML modellieren
Wie kann man nun die Klasse „Note“ so weit in UML-Modellen abbilden, dass alle Informationen dort enthalten sind – also auch die Annotationen? Da Annotationen Instanzen von Klassen sind, bietet sich hier das UML-Element „Objekt“ an, um eine Annotation mit ihren gesetzten Eigenschaften abzubilden. Nun muss noch eine Zuordnung zu dem Element erfolgen, dem die Annotation im Code zugeordnet ist. In der Datenmodellierung gibt es primitive und komplexe Datentypen, die entweder über UML-Attribute oder über Kompositions-Konnektoren (engl. Composition) modelliert werden. Glücklicherweise bietet die UML an, dass sich Konnektoren auch an UML-Attribute oder mit anderen Konnektoren verbinden lassen. Damit kann man das Modellierungsproblem lösen.

In Abbildung 3 ist das UML-Modell eines einfachen Datenmodells für eine Notizzettelverwaltungssoftware zu sehen. Es besteht aus der Klasse Notes (Abbildung 4) und der Klasse Note aus Abbildung 2. Die Annotationen (gelb eingefärbt) wurden mit Konnektoren den Attributen zugeordnet. Für den komplexen Datentyp der mit Hilfe der Kompositionsbeziehung modelliert ist, werden die Annotationen durch Konnektoren (hier Assoziationen) zur Komposition zugeordnet. Das Diagramm enthält damit gleich zwei der eher ungewöhnlichen UML-Konnektoren – Konnektoren von Unterelementen, sowie von einem anderen Konnektor.
Der Quellcode der Klasse Notes ist in Abbildung 4 dargestellt.
|
1 2 3 4 5 6 |
public class Notes { [XmlElement("Note")] [JsonProperty("notes")] public List<Note> NoteElements { get; set; } = new List<Note>(); } |
Der komplette Quellcode des Datenmodells der Notizverwaltung findet sich unter https://github.com/oalt/MDD4All.Notes.DataModels
Konnektoren per Programmcode erzeugen
Wenn man solche Konnektoren per Programmcode über das Enterprise Architect API erzeugen möchte, dann braucht dies auch eine etwas ungewöhnliche Herangehensweise. Ich habe über die letzten Jahre eine Sammlung von Hilfsmethoden für die Programmierung mit dem Enterprise Architect API zusammengetragen. Das Projekt ist als Open Source C# Code verfügbar und auch über Nuget abrufbar (https://github.com/oalt/MDD4All.EnterpriseArchitect.Manipulations). Das Projekt besteht aus einer Reihe von C#-Erweiterungsmethoden, die das Standard-EA-API erweitern, so dass man es etwas leichter hat Modellelemente zu lesen und zu verändern. Ich habe es nun kürzlich um Methoden ergänzt, die die oben beschriebenen speziellen Konnektoren anlegen können.
Konnektor zwischen einem UML-Attribut und einem Element
Um einen Konnektor zwischen einem UML-Attribut und einem anderen Modellelement anzulegen muss folgendes in der Datenbank passieren:
- Zunächst den Konnektor zwischen dem Element, welches das Attribut enthält und dem Zielelement erzeugen
- Damit der Konnektor jetzt dem Attribut zugeordnet wird, muss in der Datenbank in das Feld „StyleEx“ die GUID (Global Unique ID) des Attributs eingetragen werden und zwar in folgender Form:
LFSP=<guid>R;
bzw.
LFEP=<guid>L;
Dabei bezieht sich „LFSP“ auf das Quellelement (source/client) und „LFEP“ auf das Zielelement (target/supplier).
Die Erweiterungsmethode zum Anlegen eines solchen Konnektors sieht dann folgendermaßen aus (Abbildung 5):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/// <summary> /// Create a connector between the attribute and another model element. /// (linked to element feature 'attribute') /// </summary> /// <param name="attribute">The attribute.</param> /// <param name="repository">The EA repository.</param> /// <param name="targetElement">The target element for the connector end (supplier).</param> /// <param name="connectorType">The connector type.</param> /// <returns>The created connector.</returns> public static EAAPI.Connector AddConnector(this EAAPI.Attribute attribute, EAAPI.Repository repository, EAAPI.Element targetElement, string connectorType) { // get the element that contains the attribute EAAPI.Element element = repository.GetElementByID(attribute.ParentID); // add the connector to the element EAAPI.Connector result = element.AddConnector(targetElement, connectorType); // set StyleEx of the connector. This will link the connector to the attribute result.StyleEx = "LFSP=" + attribute.AttributeGUID + "R;"; result.Update(); return result; } |
Ein Konnektor ausgehend von einem Konnektor
Um einem Konnektor ausgehend von einem anderen Konnektor zu einem Modellelement zu ziehen sind eine Reihe von Schritten notwendig. Da die Enterprise Architect nur vorsieht, dass ein Konnektor zwischen zwei Modellelementen gezogen werden kann, wird sich hier eines kleinen Tricks bedient: Man legt ein quasi unsichtbares Element in der Datenbank an und verankert ein Ende des Konnektors dort. Dieses Element hat den Typ und Namen „ProxyConnector“.
Damit dieses Element mit dem Konnektor, für das es der Stellvertreter ist, verbunden werden kann, muss im Datenbankfeld „Classifier_guid“ des ProxyConnector-Elements die GUID des Konnektors eingetragen werden, dessen Stellvertreter es ist.
Leider lässt sich das Feld „Classifier_guid“ bisher noch nicht über die Enterprise Architect API erreichen. Man kann jedoch mit dem Befehl
repository.Execute(string sql);
einen direkten SQL-Befehl an die Datenbank senden und so das Feld trotzdem setzen.
Die Erweiterungsmethode zu Erstellung eines Konnektors zwischen einem bestehenden Konnektor und einem Modellelement sieht daher wie folgt aus (Abbildung 6):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public static EAAPI.Connector AddConnector(this EAAPI.Connector sourceConnector, EAAPI.Repository repository, EAAPI.Element targetElement, string type) { EAAPI.Connector result = null; // get the source element package to add the 'ProxyConnector' element there EAAPI.Element sourceElement = repository.GetElementByID(sourceConnector.ClientID); EAAPI.Package package = repository.GetPackageByID(sourceElement.PackageID); // create the 'ProxyConnector' element EAAPI.Element proxyConnectorElement = package.AddElement("ProxyConnector", "ProxyConnector"); // set the ClassifierGUID of the proxy connector element with the guid of the connector where the new connector should start repository.Execute("UPDATE t_object SET Classifier_guid='" + sourceConnector.ConnectorGUID + "' WHERE Object_ID=" + proxyConnectorElement.ElementID + ";"); // add the connector result = proxyConnectorElement.AddConnector(targetElement, type); return result; } |
Zusammenfassung
Manchmal braucht man eher ungewöhnliche Konnektoren in der UML-Modellierung. Es ist möglich auch solche durch das Enterprise Architect-API zu erzeugen. Man muss nur wissen wie. Mit etwas reverse Engineering in der Datenbank konnte ich herausfinden welche Schritte und Daten notwendig sind und dies mit den vorhandenen API-Funktionalitäten umzusetzen. Zwei Vorgehensweisen habe ich hier nun beschrieben.