
Teilen Sie:
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.
Steigern Sie Ihre Produktivität mit modellgetriebener Entwicklung (Teil 2)
Einführung
Willkommen zurück zu unserem produktivitätssteigernden Abenteuer! Unter Teil 1haben wir uns mit den wichtigsten Concepts des Model-Driven Engineering (MDE) beschäftigt und einige "Handwerkszeuge" kurz vorgestellt. In diesem Artikel werde ich eine Fallstudie vorstellen - insbesondere, wie ich diese Technologien eingesetzt habe, um viel Zeit beim Hinzufügen von Unterstützung für neue APIs zum Java-SDK von Vonage.
Der Schauplatz: Problemstellung
Wir haben eine Menge APIs bei Vonage. Einige davon sind ziemlich klein und einfach, aber einige sind riesig. Nehmen Sie zum Beispiel unsere größeren APIs wie Video, Besprechungen und Proaktives Verbinden. Vergleichen Sie sie mit einer kleineren API, wie Numbers Einsicht v2 - Sehen Sie sich nur den Größenunterschied in den Bildlaufleisten an! Diese größeren APIs haben nicht nur viele Endpunkte, sondern auch große und komplexe Datenmodelle für Anfragen und Antworten.
Sobald eine API als stabil angesehen wird (Status "Allgemeine Verfügbarkeit"), wollen wir sie in unseren offiziellen SDKs unterstützen. unseren offiziellen SDKs. Ich habe bereits über den Mehrwert von SDKs geschriebengeschrieben, und mein Kollege Jim Seconde hat auch darüber gesprochenDaher möchte ich die Vorteile des Angebots von SDKs für APIs nicht wiederholen. Es versteht sich von selbst, dass die Entwicklung und Pflege eines qualitativ hochwertigen SDK erhebliche Ressourcen erfordert - sie macht wahrscheinlich etwa 80 % meiner Arbeit aus! Ein Großteil des Aufwands, der mit dem Hinzufügen neuer APIs zu unseren SDKs verbunden ist, ist jedoch recht mühsam und erfordert nur wenig Nachdenken. Das Schreiben von Standardtexten kann zwar eine gewisse therapeutische Wirkung haben, ist aber zweifellos nicht die beste Verwendung der Zeit eines Entwicklers, da eine solche Aufgabe automatisiert werden kann.
SDK-Implementierungsanforderungen
Was bedeutet also ein stark typisiertes SDK, wie das Java oder .NET erfordern? Zunächst einmal muss (in der Regel) jeder Endpunkt unterstützt werden, d. h. die Logik für die richtige URL, HTTP-Anforderungsmethode und den Authentifizierungstyp muss vorhanden sein. Generierung eines JSON-Web-Tokenund dessen Anwendung auf den Request-Payload mit den richtigen Einstellungen sowie anderen Metadaten wie Content-Type und Accept Kopfzeilen. Dann gibt es noch die eigentlichen Anfragekörper. Manchmal ist die Nutzlast Teil der Abfrageparameter, z. B. beim Filtern von Suchergebnissen in einer GET Anfrage. In anderen Fällen ist sie Teil des Körpers und muss als JSON serialisiert werden. Das SDK muss auch Antworten verarbeiten: sowohl erfolgreiche (2xx HTTP-Statuscodes) als auch erfolglose (4xx- und 5xx-Codes). Endpunkte, die einen Antwortkörper zurückgeben, müssen aus JSON geparst werden. Daher muss das SDK in der Lage sein, JSON-Payloads zu serialisieren und in ein Objekt zu deserialisieren. Während Bibliotheken wie Jackson diesen Prozess deklarativ gestalten, müssen wir die Klassen und Felder immer noch manuell definieren. Es gibt noch andere Dinge, wie Validierung (um 422-Antworten zu verhindern) und Dokumentation, aber ich hoffe, Sie verstehen die Idee. Letztendlich geht es darum, den Benutzern der API das Leben so einfach wie möglich zu machen, wenn sie sie in einer Programmiersprache verwenden.
MDE-Lösung
Nachdem wir nun die Problemdomäne und die Concepts des Model-Driven Engineering vorgestellt haben, können wir uns nun daran machen, alles zusammenzuführen. Je nach Umfang und Zeitaufwand gibt es verschiedene Möglichkeiten, dies zu tun, und es gibt keinen endgültig "richtigen" Weg. Daher werde ich den pragmatischen Ansatz beschreiben, den ich gewählt habe. Da das Ziel darin besteht, die Produktivität zu maximieren, habe ich nicht viel Zeit mit dem "Over-Engineering" oder der Architektur der Lösung im Vorfeld verbracht. Bei MDE kann das funktionieren, aber es hat auch seine Nachteile. Darauf werde ich am Ende noch einmal eingehen. Wenn Sie das Ganze verfolgen möchten, habe ich es als Open-Source auf GitHub.
Metamodell
Wie bei jedem modellgesteuerten Entwicklungsansatz ist der erste Ausgangspunkt das Metamodell. Hier ist es also, mit der textuellen Emfatic-Syntax auf der linken Seite und der Baumansicht auf der rechten Seite. Beachten Sie, dass ich mich entschieden habe, das Metamodell mit dem eingebauten Ecore-Editor zu erstellen und nicht mit Emfatic oder einem anderen (Meta-)Modellierungswerkzeug.

Wenn Sie sich eine der oben erwähnten OpenAPI-Spezifikationen angesehen haben, sollte das Metamodell in seiner Struktur hoffentlich einigermaßen selbsterklärend sein. Die Wurzel der Hierarchie ist die Api Klasse, die eine name, package (was bedeutet, wo sich die Klassen im SDK befinden werden), der Basisendpunktpfad (z. B., https://api-eu.vonage.com/v1/meetings für Meetings API) und natürlich die eigentlichen Endpunkte. Da alle Objekte im Root-Element enthalten sein müssen, types hier referenziert, auch wenn sie nicht direkt von der Api Klasse verwendet werden.
Die Endpoint Klasse ist die nächste in der Hierarchie. Sie enthält, was man erwarten würde: Name, URL (Pfad), die HTTP-Anforderungsmethode (dargestellt als Enum), eine oder mehrere Authentifizierungsmethoden (wiederum dargestellt als Enum, da es drei Typen gibt) und natürlich die Anfrage- und Antworttypen.
Was ist also ein Typ? Nun, es kann ein eingebauter Typ sein, auf den wir verweisen wollen - d.h. ein Typ, der bereits im SDK, in der Standardbibliothek usw. definiert ist. - im Grunde alles, was wir nicht modellieren wollen. Also hat ein Type hat einfach ein name Attribut, das wir in diesen Fällen verwenden können (z.B. String, UUID, Integer, URI und so weiter). Typen, die wir modellieren wollen, sind ein Class der die Typen Type erweitert (und somit das name Attribut). Dies ist eine Art Übung zur teilweisen Modellierung einer Java-Klasse, nur viel spezifischer für unsere Bedürfnisse. Wie Sie an den übrigen im Metamodell beschriebenen Attributen und Typen sehen können, gibt es einige sehr merkwürdige Felder und bemerkenswerte Auslassungen. Eine Java-Klasse hat zum Beispiel Methoden und Konstruktoren, die hier aber nicht enthalten sind. Und warum? Weil wir sie nicht brauchen. Ich habe auch beschlossen, die Dokumentation für Class und Field mit Hilfe des Documentation Typs zu modellieren, aber auch diese sind im Vergleich zu den Möglichkeiten von Javadoc unvollständig. Die Modellierung der Gesamtheit von Java geht weit über unsere Bedürfnisse hinaus. Um Ihre Neugierde zu befriedigen, hier ist ein Metamodell von Java 7 - und das ohne die ganzen ausgefallenen Funktionen der neuesten JDKs!
Code-Generator
Jetzt kommt der Teil, auf den Sie gewartet haben: die Codegenerierung! Wie in Teil 1 erwähnt, gibt es mehrere Model-to-Text-Tools, aber ich habe mich für folgende entschieden Epsilon's Generierungssprache (EGL). Das liegt vor allem an der Vertrautheit, und EGL ist nichts Besonderes. Es handelt sich um eine vorlagenbasierte Sprache, bei der eine Vorlage (.egl Datei) über statische und dynamische Bereiche hat. Statische Bereiche sind die Standardeinstellung: Der in die Datei eingegebene Text wird wortwörtlich in der Ausgabe erscheinen. Dynamische Bereiche hingegen ermöglichen es Ihnen, die Ausgabe programmatisch zu bestimmen, die von einer Eigenschaft des Modells abhängen kann (und in der Regel auch wird).
Zum Beispiel, in exception.eglverwende ich nur eine Eigenschaft: name um die Ausgabe zu variieren. Die request_response.egl Vorlage ist komplexer und verwendet for Schleifen, um alle Felder in der Klasse zu deklarieren, und stützt sich auf Hilfsfunktionen, die ich in helper_functions.egl. Da EGL auf EOL aufbaut, können dynamische Regionen jeden EOL-Code enthalten, auch Operationen. EGL hat auch "Template-Operationen" (annotiert mit @template), die Funktionen sind, die Text ausgeben (oder den Text als String zurückgeben), wenn sie aufgerufen werden. Auf diese Weise können wir unsere Bibliothek von Hilfsfunktionen aufbauen und sie in verschiedenen Vorlagen wiederverwenden.
Sie fragen sich vielleicht: Woher kommen diese Variablen, und wie werden die Vorlagen aufgerufen? Damit eine Vorlage nützlich ist, muss sie mit Werten aus dem Modell parametrisiert werden. Und wohin wird die Ausgabe geschrieben? Hier liegt der Hauptvorteil der Verwendung von Epsilon: seine EGX-Koordinierungssprache. Die Idee von EGX ist es, eine regelbasierte Sprache bereitzustellen, die steuert, wann und wie Vorlagen aufgerufen werden, mit welchen Parametern sie aufgerufen werden und wohin die Ausgabe geleitet wird. Die specs.egx Datei definiert diese Logik - sie ist das, was das Modell und die Vorlagen zusammenbringt. Der pre Abschnitt wird zuerst ausgeführt und enthält hauptsächlich Variablendeklarationen, die im Skript verwendet werden, wie z. B. das Ausgabeverzeichnis und allgemeine Namen. Er ruft auch verschiedene Typen aus dem Modell ab und kategorisiert sie nach Eigenschaften, z. B. ob es sich um Anfragen oder Antworten handelt (wir haben dies in unserem Metamodell als boolesche Eigenschaft von Class in unserem Metamodell definiert). Operationen, die in diesem Skript für den Typ Class in diesem Skript deklarierten Operationen werden verwendet, um die Variablen, die an jede Vorlage übergeben werden, aus den Eigenschaften des Modellelements abzuleiten.
Um dies zu verdeutlichen, lassen Sie uns ein Beispiel anführen. Nehmen wir die QueryParamsRequest-Regel. Denken Sie an die transform und in Schlüsselwörter wie eine erweiterte for Schleife. Die Eingabesammlung stammt in diesem Fall aus der queryParamsRequestTypes berechnet in pre. In anderen Fällen (zum Beispiel, die Enum Regel) stammt sie von allen Instanzen eines bestimmten Typs im Modell. Für jedes Modellelement (in unserem Beispiel, gebunden an die Variable request), werden die parameters aus einer Hilfsmethode gewonnen. Diese bindet Variablennamen, die in der Vorlage verwendet werden sollen, an die Eigenschaften von request - Beachten Sie, dass die verwendeten Operationen self da die Operation auf den Typ Classdeklariert ist, der die Attribute hat, die wir verwenden wollen. Die template ist ein relativer Pfad zu der EGL-Vorlage, die wir für diese Regel aufrufen wollen. Schließlich ist die target die Datei, in die wir die Ergebnisse schreiben wollen. Standardmäßig überschreibt EGX die Datei, wenn sie existiert, aber dies ist konfigurierbar.
Ich hoffe, das macht jetzt langsam Sinn! Sie können sehen, wie wir mit EGX auswählen können, welche Vorlagen auf der Grundlage von Modellelementtypen aufgerufen werden sollen und wie die Variablen dieser Vorlagen vom Modell abgeleitet werden. Diese Koordinationslogik ist das eigentliche Herzstück des Ansatzes. Um sie auszuführen, füttern wir die Vorlage einfach mit dem gewünschten Modell bzw. den gewünschten Modellen und erhalten die Ausgabe.
Das ist alles für den Moment...
In diesem Artikel wurde kurz der MDE-Ansatz erläutert, der für den speziellen Fall der Erstellung von Boilerplate für das Java SDK gewählt wurde. Ich hoffe, der Mehrwert ist offensichtlich, aber vielleicht interessieren Sie sich mehr für den Entwicklungsprozess und die Lektionen, die Sie aus dieser Übung gelernt haben. Im dritten und letzten Teil dieser Serie werde ich über den Ansatz nachdenken und Ihnen einige wichtige Erkenntnisse vermitteln, die Sie berücksichtigen sollten, wenn Sie diesen Weg für Ihre Projekte einschlagen wollen.
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:
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.