https://d226lax1qjow5r.cloudfront.net/blog/blogposts/boost-your-productivity-with-model-driven-engineering-part-3/boost-productivity_model-driven-engineering_p3.png

Steigern Sie Ihre Produktivität mit modellgetriebener Entwicklung (Teil 3)

Zuletzt aktualisiert am January 31, 2024

Lesedauer: 13 Minuten

Einführung

Willkommen zurück zum Finale dieser Serie über modellgestützte Technik. Unter Teil 2habe ich beschrieben, wie ich diese Technologien eingesetzt habe, um viel Zeit beim Hinzufügen von Unterstützung für neue APIs zum Vonage Java SDK. In diesem Artikel werde ich einige Schlüsselprinzipien hervorheben, die auf den gemachten Erfahrungen basieren, damit Sie das Beste aus den Technologien und dem Ansatz machen können.

Kenne dein "Warum"

Bevor Sie weitermachen, sollten Sie sich vergewissern, dass dies für Ihren Anwendungsfall geeignet ist. Wie das Sprichwort sagt: "Wenn man nur einen Hammer hat, sieht alles wie ein Nagel aus". Es ist normal, dass Technologien Moden durchlaufen - dies ist bekannt als der "Gartner-Hype-Zyklus".. Bestimmte Technologien und Entwicklungsmethoden scheinen zwar verlockend zu sein, aber letztendlich sind sie nur Werkzeuge. Ein breit gefächerter Werkzeugkasten ist großartig, denn er ermöglicht es uns, das richtige Werkzeug oder den richtigen Ansatz für die jeweilige Aufgabe zu wählen. Was für einen Anwendungsfall das Richtige ist, muss nicht unbedingt auch für einen anderen gelten. Wann ist MDE also die richtige Wahl und wann nicht? Hier sind einige Fragen, die zu berücksichtigen sind.

Ist der Bereich leicht zu modellieren?

Nicht alle Herausforderungen lassen sich in ein einfach zu definierendes Metamodell einpassen. Während es in vielen Fällen möglich ist, eine objektorientierte Sichtweise einer Domäne mit vorhersehbaren Beziehungen zwischen den Konzepten in der Domäne einzunehmen, kann es vorkommen, dass die Art dieser Konzepte und sogar ihre Beziehung zueinander zu dynamisch sind. Um den größtmöglichen Nutzen aus einem modellbasierten Ansatz zu ziehen, sollte das Metamodell des Fachgebiets zumindest mit einem hohen Maß an Sicherheit definierbar sein. Die Concepts sollten auf Klassen mit definierbaren Attributen und Beziehungen abgebildet werden. Wenn es zu viele zu definierende Domänenelemente gibt und die Beziehungen zu komplex sind, wird sich dies in Ihrem Metamodell widerspiegeln. In solchen Fällen ist die Definition des Metamodells und der Modelle eine zu große Aufgabe und kann die Vorteile gegenüber einer direkten Implementierung überwiegen.

Wie komplex wird der Generator sein?

Wenn Ihre Domäne mithilfe eines Metamodells sauber definiert werden kann, müssen Sie auch überlegen, wie sich diese Concepts auf den Code und die Geschäftslogik abbilden, die Sie generieren möchten. Es gibt Argumente für beide Extreme: Wenn die Logik dicht ist (d.h. in hohem Maße vom Modell abhängt), dann verringert die automatische Generierung die Wahrscheinlichkeit von Fehlern. Andererseits kann es den Generator schwieriger machen, ihn zu pflegen und mit ihm zu arbeiten. Es lohnt sich auch zu fragen, wie viele Vorlagen Sie entwickeln und pflegen müssen und wie komplex die Koordinationslogik für den Aufruf dieser Vorlagen insgesamt ist, insbesondere im Vergleich zu handgeschriebenem Code.

Wie viel manuelle Kodierung wird dadurch eingespart?

Eine der wichtigsten und vielleicht auch einfachsten Fragen ist die nach dem Verhältnis von "Boilerplate" zu Logik. Ein modellgesteuerter, schablonenbasierter Codegenerierungsansatz funktioniert hervorragend, wenn der generierte Code größtenteils aus Textbausteinen besteht, so dass die Ausgabe mit minimalen oder sogar gar keinen Änderungen in Ihre Codebasis eingefügt werden kann. Wenn der generierte Code jedoch einen erheblichen zusätzlichen manuellen Programmieraufwand erfordert - vor allem, wenn es sich um eine Änderung des generierten Codes handelt, anstatt z. B. neue Methoden hinzuzufügen - dann sinkt der Wert des Generators. Der Grund dafür ist, dass der Aufwand für die Entwicklung und Pflege des Generators sowie für das Refactoring oder die manuelle Änderung des generierten Codes für einige Entwickler eine Belastung darstellen kann, verglichen mit der Möglichkeit, diese Teile des Codes von Grund auf neu zu schreiben.

Wie häufig werden Sie es benutzen?

Selbst wenn ein modellgesteuerter Ansatz machbar ist - d. h. Ihr Bereich passt genau in ein Metamodell, und die Codegenerierung ist vorteilhaft - lohnt sich der Aufwand möglicherweise nicht, wenn Sie ihn nur einmal verwenden wollen. Natürlich hängt dies auch von anderen Faktoren ab, z. B. davon, wie viel Code Sie generieren - wenn es sich um ein großes Projekt handelt, kann sich die Investition durchaus lohnen, selbst wenn es sich um eine einmalige Anwendung handelt. In anderen Fällen sind die "Einsparungen" vielleicht bescheiden, summieren sich aber im Laufe der Zeit durch häufige Nutzung. Eine andere Art, über diese Frage nachzudenken, ist: "Wie viele Modelle werden wir haben?" - die Chancen stehen gut, dass Sie den Generator umso mehr nutzen werden, je mehr Modelle Sie erstellen (und je häufiger Sie die Modelle ändern).

Wie einfach ist es, Ihr(e) Modell(e) zu aktualisieren?

Im vorigen Punkt habe ich erwähnt, dass Sie die Vorteile eines Codegenerierungsansatzes besser nutzen können, wenn Sie mehr Modelle haben oder häufig wechselnde Modelle verwenden. Wenn Ihre Modelle jedoch nur für diese Übung erstellt werden und ständig aktualisiert werden müssen, um veränderte Geschäftsanforderungen oder die Umgebung widerzuspiegeln, kann dies zu Problemen führen. In meinem Fall zum Beispiel muss das Modell bei jeder Änderung der API-Spezifikation aktualisiert werden. Der Aufwand für die Aktualisierung des Modells ist direkt proportional zum Umfang der Änderungen in der Spezifikation. Wenn die Modelle schnell weiterentwickelt werden müssen, bedeutet dies, dass jeder zuvor generierte Code nun ungültig ist und neu generiert werden muss. Berücksichtigen Sie dabei auch die Tatsache, dass ein Teil des generierten Codes möglicherweise von Hand geändert wurde (oder geändert werden muss), wie in einem früheren Punkt beschrieben.

Wird das Metamodell häufig geändert werden müssen?

Da es sich bei Modellen um strukturierte Daten handelt, ist zu erwarten, dass sie sich ändern - manchmal sogar schnell. Das ist meist in Ordnung und relativ reibungslos, wenn die Aktualisierung des Modells automatisiert wird. Was sich jedoch nicht häufig ändern sollte, ist das Schema (Metamodell). Dies ähnelt in gewisser Weise dem ersten Punkt: Wenn sich die Concepts in Ihrem Bereich häufig ändern und es schwierig ist, ein stabiles Metamodell festzulegen, dann wird es sehr viel schwieriger, einen modellgesteuerten Ansatz zu verfolgen. Idealerweise sollte das Metamodell vor allem während der Prototyping-Phase überarbeitet werden und nur selten, sobald es stabil und "in Produktion" ist (d. h. die Ausgabe ist brauchbar). Das Hinzufügen von Feldern zum Metamodell ist in der Regel keine bahnbrechende Änderung, da sie optional sein können, aber das Entfernen von Klassen, Relationen und Attributen kann (und wird in der Regel) Ihre bestehenden Modelle und Arbeitsabläufe zerstören, was bedeutet, dass Sie in einigen Fällen von vorne beginnen müssen.

Wie häufig wird sich der Generator abgesehen von Fehlerkorrekturen ändern?

Während der anfänglichen Entwicklung Ihrer Codegeneratorvorlagen und der Koordinationslogik kann es natürlich zu vielen Fehlern oder Auslassungen kommen. Sobald diese größtenteils beseitigt sind und die generierte Ausgabe wie gewünscht ist, kann der Generator als stabil angesehen werden. Aber auch danach wird es unweigerlich zu Änderungen kommen, die auf neue Geschäftsanforderungen, Styleguides oder die Codestruktur zurückzuführen sind. Was auch immer der Grund sein mag, es ist zu bedenken, dass jede Änderung an einem stabilen Generator das Risiko birgt, neue Fehler einzuführen, die wiederum ausgebügelt werden müssen. Da diese in der Regel nur bei der Inspektion des ausgegebenen Codes entdeckt werden können, wäre es ideal, wenn der Generator nicht häufig geändert werden müsste. Das liegt daran, dass der Generator schwieriger zu testen ist (oder sein kann) als der eigentliche generierte Code, so dass sich schnell ändernde Geschäftsanforderungen, die sich in der Template-Logik widerspiegeln müssen, zusätzliche Reibung verursachen.

Ist Ihr Team damit einverstanden?

Zu guter Letzt: Selbst wenn ein modellgesteuerter Ansatz für Ihren Anwendungsfall perfekt zu passen scheint, ist es unwahrscheinlich, dass er erfolgreich ist, wenn Ihr Vorgesetzter und Ihre Kollegen nicht bereit sind, diesen Workflow zu unterstützen. Schließlich ist die Erstellung des Metamodells, des Generators und der Modelle sowie deren Pflege eine zusätzliche Belastung, die die Komplexität erhöht und das Verständnis der verwendeten Technologien voraussetzt. Auch organisatorische Faktoren spielen eine Rolle - z. B. woher die Modelle kommen, wer und wie sie erstellt oder aktualisiert werden, wer für den Generator verantwortlich ist, welche Art von Prozess verwendet wird, um den generierten Output zu extrahieren und in die bestehende Codebasis einzufügen usw. Es gibt viele logistische Herausforderungen, die bei diesem Ansatz zu berücksichtigen sind, ganz zu schweigen von der Einhaltung der Vorschriften und der Prüfung! Wenn Sie diesen Ansatz verwenden, um Boilerplate zu generieren, aber jeder, der ihn verwendet, verspricht, den generierten Code manuell zu inspizieren und zu überprüfen, bevor er in die Hauptcodebasis aufgenommen wird, sollte es relativ wenig Reibung geben. Wenn es sich jedoch um einen integralen und obligatorischen Teil des Entwicklungsprozesses mit Automatisierung handelt, sind tiefere Überlegungen und Sorgfaltspflichten erforderlich. In jedem Fall sollten die Personen, die die Modelle, den Generator oder die daraus resultierende Ausgabe verwenden, mit dem Ansatz vertraut sein und ihn unterstützen.

Iterative Entwicklung

Angenommen, Sie haben über alle oben genannten Fragen nachgedacht und festgestellt, dass der Ansatz für Ihren Anwendungsfall geeignet ist. Wie sollten Sie vorgehen, um es zu entwickeln? Ausgehend von dem, was ich bisher dargelegt habe, haben Sie vielleicht den Eindruck, dass modellgetriebene Entwicklung ein "Wasserfall"-Prozess ist. Es stimmt zwar, dass das Metamodell stabil sein sollte und natürlich der Ausgangspunkt ist, aber in der Praxis gibt es mehr Flexibilität, als die Literatur und die Werkzeuge vermuten lassen.

In meinem Fall habe ich sicherlich nicht mit dem perfekten Metamodell begonnen, und selbst jetzt hat es noch erheblichen Raum für Verbesserungen. Bei der Erstellung von Modellen habe ich zum Beispiel festgestellt, dass ich integrierte Java-Typen erstellen muss, um auf sie verweisen zu können, die aber Teil des Metamodells sein könnten. Viele der Attribute im Modell - insbesondere boolesche Attribute wie isRequest, isResponse, isHal, isQueryParams usw. - sind entstanden, als mir klar wurde, dass sie vom Generator benötigt werden würden. Additive Änderungen sind kein Beinbruch - Sie können Ihr Metamodell in der Regel um neue Typen und Attribute erweitern, solange sie optional sind oder einen sinnvollen Standardwert haben.

Modellgetriebene Entwicklung ist immer noch Softwareentwicklung, und ein iterativer Prozess ermöglicht es Ihnen, schneller Feedback zu erhalten, anstatt im Vorfeld zu viel zu investieren, wenn Sie weniger Informationen haben. Daher empfehle ich in der Praxis, das Metamodell, den Generator und ein Beispielmodell parallel zu entwickeln, damit Sie den Arbeitsablauf von Anfang bis Ende sehen und fehlende Parameter in Ihrem Metamodell sowie Fehler im generierten Text frühzeitig erkennen können und nicht erst, nachdem Sie das entwickelt haben, was auf dem Papier "vollständig" zu sein scheint.

"One-Shot" oder "reine" MDE?

Der Anwendungsfall, den ich in Teil 2 vorgestellt habe, war ein "einmaliger" Ansatz. Das heißt, sobald der Code generiert wurde, habe ich keine Verwendung mehr für den Generator oder das Modell. Wenn z. B. die API-Spezifikation aktualisiert wird oder ich einige Fehler im ausgegebenen Code entdecke - es sei denn, es gibt größere Probleme oder Überarbeitungen - werde ich das Modell wahrscheinlich nicht aktualisieren und den Code neu generieren, weil ich den generierten Code bereits überarbeitet und weiterentwickelt habe. Schließlich ist die Ausgabe das, was für mich wichtig ist und was ich beibehalten werde. In diesem Sinne habe ich MDE nicht wirklich so praktiziert, wie man es eigentlich tun sollte. Der generierte Code ist ein Ausgangspunkt, auf dem man aufbauen kann, nicht das fertige Produkt. Einige mögen daher argumentieren, dass es nicht modellgetrieben ist. Das heißt aber nicht, dass es nicht nützlich ist.

Bei einem traditionellen modellgesteuerten Ansatz würde der Code idealerweise jedes Mal, wenn das Modell aktualisiert wird, aus dem Modell neu generiert werden, damit die Codebasis und das Modell immer synchron sind. Stellen Sie sich das einmal so vor: Wenn Sie ein Programm in einer JVM-Sprache (Java, Kotlin, Groovy, Scala usw.) schreiben, was übertragen Sie dann in Ihre Versionsverwaltung? Was pflegen Sie? Was ist Ihnen wichtig? Es ist der Quellcode. Aber der Quellcode ist nicht das, was zur Laufzeit zählt; es ist der Bytecode (d. h. die .class Dateien). Warum ist uns unser Code so wichtig, wenn er doch sowieso nur zu Java-Bytecode kompiliert wird? Weil der Bytecode deterministisch abgeleitet aus dem Quellcode abgeleitet ist. Man könnte argumentieren, dass der Hauptvorteil dieser Sprachen der Compiler ist. Nun, ein Compiler ist im Wesentlichen ein Transformator: Ihr Quellcode ist das Modell (das dem Metamodell der Sprache entspricht), der Compiler ist der Generator, und der Bytecode ist die Ausgabe des Generators (Ich habe bereits früher darüber geschrieben, falls Sie neugierig sind). In ähnlicher Weise sollte der generierte Code in "reiner MDE" genauso behandelt werden wie kompilierter Code: notwendig, aber nicht etwas, das wir uns ansehen oder um das wir uns groß kümmern müssen. Die Generatorvorlagen sind im Wesentlichen der "Quellcode" in MDE.

Es ist wichtig, dies im Zusammenhang mit den zuvor gestellten Fragen zu betrachten, denn die manuelle Synchronisierung zwischen Modell und Quellcode ist eine Quelle von Reibung und potenziellen Fehlern. Wenn Sie einen pragmatischeren Ansatz verfolgen, bei dem Sie einen ersten Code erhalten möchten und nicht vorhaben, den Generator mit demselben Modell wiederzuverwenden, dann ist dies weniger problematisch. Nehmen Sie außerdem an, dass der generierte Code nicht viel (oder gar keine) manuelle Änderung benötigt. In diesem Fall können Sie das Modell oder den Generator aktualisieren, wenn Sie Fehler im Ausgabecode entdecken, und ihn mit minimalem manuellen Aufwand neu generieren. Wenn hingegen der Ausgabecode nach der Generierung erheblich von Hand geändert werden muss, lohnt es sich wahrscheinlich nicht, das Modell zu aktualisieren und neu zu generieren, da Sie die manuellen Änderungen erneut mit dem generierten Code abstimmen müssen.

Müll rein, Müll raus

Das bringt mich zu meinem nächsten Punkt. Bei diesem Ansatz kommt nur das heraus, was man hineinsteckt. Das gilt sowohl für das/die Modell(e) als auch für den Generator. Wenn Sie bei der Erstellung Ihres Modells an allen Ecken und Enden sparen, indem Sie nicht benötigte Felder leer lassen, kann der generierte Code nicht die gewünschte Ausgabe haben. In meinem Beispiel war ich bei der Dokumentation und den für die Tests verwendeten Werten manchmal nachlässig, da ich davon ausging, dass ich sie ohnehin anpassen müsste. Aber wenn ich mir mehr Mühe gegeben habe, musste ich weniger Änderungen vornehmen. Wie bereits erwähnt, können Sie Ihre Generatorvorlagen jederzeit ergänzen oder optimieren, wenn Sie feststellen, dass zusätzliche Funktionen benötigt werden oder es Formatierungsfehler gibt. Wenn Sie sich die Zeit nehmen, um sicherzustellen, dass Ihr Modell korrekt und so vollständig wie möglich ist, verringern Sie Fehler und die Notwendigkeit, den Generator neu zu starten. Dies ist besonders wichtig bei einmaligen Ansätzen oder wenn der generierte Code nach der ersten Generierung manuell bearbeitet wird.

Es gilt das Pareto-Prinzip

Die 80/20-Regel trifft meiner Erfahrung nach sehr gut auf die modellbasierte Codegenerierung zu und unterstreicht das Argument der iterativen Entwicklung. Sie können etwa 80 % der Vorteile mit 20 % des Aufwands erzielen. Wenn Sie mit einfachen Vorlagen beginnen, bei denen Sie auch nur das Grundgerüst der benötigten Klassen generieren, haben Sie bereits einen großen Teil des Codes geschrieben. Sie haben die Klassen mit den richtigen Namen im richtigen Paket, mit dem Copyright-Header, typischen Importen, Methoden, Felddeklarationen, Konstruktoren usw. Sie können dann nach und nach die kompliziertere handgeschriebene Logik in den Generator einbauen, wenn Sie sie für repetitiv halten.

Beginnen Sie immer zuerst mit den niedrig hängenden Früchten und versuchen Sie nicht, zu viel auf einmal zu tun. Dies gilt insbesondere für die Generierung von Testcode: Das Schreiben eines Generators, der nur die Testklassen und Methoden-Stubs mit den Assertions erstellt, sparte viel Zeit. Ich konnte mich dann darauf konzentrieren, die wesentliche Testlogik von Hand zu schreiben, ohne mich um die Boilerplate zu kümmern. Dadurch wird auch Ihr Arbeitsablauf strukturiert, so dass Sie genau wissen, was zu tun ist, ohne sich mit all den kleinen Dingen herumschlagen zu müssen. Sie werden überrascht sein, wie sehr dies allein Ihre Motivation und Produktivität steigern kann!

Auf der anderen Seite ist es wichtig, dies mit den praktischen Aspekten der Pflege und Weiterentwicklung der Codebasis in Einklang zu bringen. Wenn Ihnen die Pflege des generierten Codes wichtiger ist als die des Generators und der Modelle, sollten Sie Ihren Generator nicht zu sehr überarbeiten, da Sie ihn sonst für einen sehr geringen Nutzen exponentiell komplexer machen würden. Dadurch wird der Generator einschüchternder und schwieriger zu handhaben, so dass die Einarbeitung anderer Personen zu einer Herausforderung wird. Als Faustregel gilt: Versuchen Sie, jede Generatorvorlage so zu gestalten, dass sie der generierten Ausgabe ähnelt. Angenommen, Ihre Vorlagen bestehen mehr aus dynamischen Abschnitten mit starker Verzweigungslogik als aus statischem Text. In diesem Fall lohnt es sich wahrscheinlich, diese Logik in Dienstprogrammmethoden aufzuteilen oder die Vorlage zu vereinfachen, um sie lesbarer zu machen.

In jedem Fall sollte man sich darüber im Klaren sein, dass es hier keine "Magie" gibt: Die Erstellung und Pflege des MDE-Workflows und der dazugehörigen Werkzeuge ist ein zusätzlicher Aufwand. Seien Sie sich bewusst, wie viel Zeit Sie im Verhältnis zu den Einsparungen, die Sie erzielen, investieren. Und damit schließt sich der Kreis zu den Gründen, die für diese Maßnahme sprechen.

Schlussfolgerung

Damit sind wir am Ende dieser Blog-Miniserie über pragmatische modellgetriebene Entwicklung angelangt. Ich hoffe, Sie haben nicht nur einige neue Werkzeuge und einen Entwicklungsansatz kennengelernt, sondern sind sich auch ihrer Grenzen bewusst und wissen, wie Sie das Beste aus diesen Technologien machen können. Letztendlich hoffe ich, dass diese Einführung in die modellgetriebene Entwicklung und die Fallstudie als nützliches Beispiel dienen und Ihnen helfen, bessere Entscheidungen in Ihren Softwareprojekten zu treffen, sollten Sie sich für diesen Weg entscheiden.

Wenn Sie Kommentare oder Vorschläge haben, können Sie sich gerne an uns wenden auf X, früher bekannt als Twitter oder besuchen Sie unseren Gemeinschaft Slack. Ich hoffe, dass dieser Artikel nützlich war und freue mich über alle Gedanken und Meinungen. Wenn er Ihnen gefallen hat, lesen Sie bitte auch meine anderen Java-Artikel.

Teilen Sie:

https://a.storyblok.com/f/270183/400x400/46a3751f47/sina-madani.png
Sina MadaniVonage Ehemaliges Teammitglied

Sina ist Java Developer Advocate bei Vonage. Er hat einen akademischen Hintergrund und ist generell neugierig auf alles, was mit Autos, Computern, Programmierung, Technologie und der menschlichen Natur zu tun hat. In seiner Freizeit geht er gerne spazieren oder spielt Videospiele.