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

Augmentez votre productivité avec l'ingénierie pilotée par les modèles (Partie 2)

Publié le January 23, 2024

Temps de lecture : 9 minutes

Introduction

Bienvenue dans notre aventure pour stimuler la productivité ! Dans la partie 1nous avons abordé les principaux concepts de l'ingénierie dirigée par les modèles (IDM) et présenté brièvement quelques "outils du métier". Dans cet article, je présenterai une étude de cas - plus précisément, comment j'ai utilisé ces technologies pour gagner beaucoup de temps lors de l'ajout de la prise en charge de nouvelles API au SDK Java de Vonage.

Planter le décor : Énoncé du problème

Nous avons beaucoup d'API chez Vonage. Certaines d'entre elles sont relativement petites et simples, mais d'autres sont énormes. Prenons, par exemple, nos API les plus importantes comme Video, Réunions et Connexion proactive. Comparez-les à une API plus petite, comme Numbers Insight v2 - regardez la différence de taille des barres de défilement ! Ces API plus importantes ont non seulement de nombreux points d'extrémité, mais aussi des modèles de données importants et complexes pour les demandes et les réponses.

Une fois qu'une API est considérée comme stable (statut "General Availability"), nous nous efforçons d'en ajouter la prise en charge dans nos SDK officiels. J'ai déjà écrit sur la valeur ajoutée des SDKet mon collègue Jim Seconde a également a également abordé ce sujetJe ne reviendrai donc pas sur les avantages qu'il y a à proposer des SDK pour les API. Il va sans dire que le développement et la maintenance d'un SDK de haute qualité nécessitent des ressources importantes - c'est probablement environ 80 % de mon travail ! Cependant, une grande partie des efforts déployés pour ajouter de nouvelles API à nos SDK est assez laborieuse et nécessite très peu de réflexion. Bien que la rédaction d'un modèle standard puisse être quelque peu thérapeutique, ce n'est sans doute pas la meilleure façon d'utiliser le temps d'un développeur, car une telle tâche peut être automatisée.

Exigences de mise en œuvre du SDK

Qu'apporte donc un SDK fortement typé, tel que le SDK Java ou .NET exige-t-il ? Tout d'abord, chaque point de terminaison doit (généralement) être pris en charge, ce qui implique de disposer de la logique pour l'URL, la méthode de requête HTTP et le type d'authentification corrects. Générer un jeton Web JSONJSON, par exemple, et l'appliquer à la charge utile de la requête avec les bons paramètres, ainsi que d'autres métadonnées telles que Content-Type et Accept . Ensuite, il y a les corps de la demande proprement dits. Parfois, les données utiles font partie des paramètres de la requête, par exemple lors du filtrage des résultats d'une recherche dans une GET dans une requête. Dans d'autres cas, il fait partie du corps de la requête et doit être sérialisé en JSON. Le SDK doit également gérer les réponses : qu'elles soient positives (codes d'état HTTP 2xx) ou négatives (codes 4xx et 5xx). Les points de terminaison qui renvoient un corps de réponse doivent être analysés à partir de JSON. Le SDK doit donc être en mesure de sérialiser et de désérialiser les charges utiles JSON en un objet. Alors que des bibliothèques telles que Jackson rendent ce processus déclaratif, nous devons toujours définir les classes et les champs manuellement. Il y a d'autres choses, comme la validation (pour éviter les réponses 422) et la documentation, mais j'espère que vous avez compris l'idée. En fin de compte, il s'agit de rendre la vie aussi facile que possible aux utilisateurs de l'API lorsqu'ils l'utilisent à partir d'un langage de programmation.

Solution MDE

Après avoir présenté le domaine du problème et les concepts de l'ingénierie dirigée par les modèles, nous pouvons maintenant envisager d'assembler le tout. Il existe plusieurs façons d'aborder cette question en fonction de la portée et du temps imparti, et il n'y a pas de "bonne" façon définitive de procéder. Je vais donc décrire l'approche pragmatique que j'ai adoptée. L'objectif étant de maximiser la productivité, je n'ai pas passé beaucoup de temps à "sur-ingénieriser" ou à architecturer la solution en amont. Avec l'EDM, cela peut fonctionner, mais cela présente aussi des inconvénients. Je reviendrai sur ce point à la fin. Si vous souhaitez suivre le processus, j'ai rendu ce document open-source sur GitHub.

Métamodèle

Comme c'est le cas pour toute approche d'ingénierie guidée par les modèles, le premier point de départ est le métamodèle. Le voici donc, avec la syntaxe Emfatic textuelle à gauche et l'arborescence à droite. Notez que j'ai décidé de créer le métamodèle en utilisant l'éditeur Ecore intégré plutôt qu'Emfatic ou un autre outil de (méta)modélisation.

API metamodel

Si vous avez consulté l'une des spécifications de l'OpenAPI mentionnées plus haut, le métamodèle devrait, je l'espère, être quelque peu explicite dans sa structure. La racine de la hiérarchie est la classe Api qui possède une classe name, package (qui indique l'emplacement des classes dans le SDK), le chemin du point d'accès de base (par exemple, https://api-eu.vonage.com/v1/meetings pour Meetings API) et, bien sûr, les points de terminaison proprement dits. Étant donné que tous les objets doivent être contenus dans l'élément racine, types sont référencés ici même s'ils ne sont pas directement utilisés par la classe Api classe.

La classe Endpoint est la classe suivante dans la hiérarchie. Elle possède ce que l'on attend d'elle : le nom, l'URL (chemin), la méthode de requête HTTP (représentée par une énumération), une ou plusieurs méthodes d'authentification (là encore, représentées par une énumération puisqu'il en existe 3 types) et, bien sûr, les types de requête et de réponse.

Qu'est-ce qu'un type ? Il peut s'agir d'un type intégré que nous voulons référencer - c'est-à-dire un type déjà défini dans le SDK, la bibliothèque standard, etc. - en fait, tout ce que nous ne voulons pas modéliser. Ainsi, un Type possède donc un attribut name que nous pouvons utiliser dans ces cas (par ex. String, UUID, Integer, URI et ainsi de suite). Les types que nous voulons modéliser sont les suivants Class qui étend Type (héritant ainsi de l'attribut name ). Il s'agit en quelque sorte d'un exercice de modélisation partielle d'une classe Java, mais beaucoup plus spécifique à nos besoins. Comme vous pouvez le voir dans les autres attributs et types décrits dans le métamodèle, il existe des champs très particuliers et des omissions notables. Par exemple, une classe Java possède des méthodes et des constructeurs, mais ceux-ci ne sont pas inclus ici. Pourquoi ? Parce que nous n'en avons pas besoin. J'ai également décidé de modéliser la documentation de Class et Field en utilisant le type Documentation mais, là encore, ces modèles sont incomplets par rapport aux capacités de Javadoc. Modéliser l'intégralité de Java est bien au-delà de nos besoins. Pour satisfaire votre curiosité, voici un métamodèle de Java 7 - et ce, sans toutes les fonctionnalités fantaisistes des derniers JDK !

Générateur de code

Passons maintenant à la partie que vous attendiez : la génération de code ! Comme mentionné dans la partie 1, il existe plusieurs outils Model-to-Text disponibles, mais j'ai choisi d'utiliser le le langage de génération d'Epsilon (EGL). Ceci est principalement dû à la familiarité, et EGL n'est pas particulièrement spécial. Il s'agit d'un langage basé sur des modèles, où un modèle (.egl ) a des propriétés statique et dynamiques statiques et dynamiques. Les régions statiques sont les régions par défaut : le texte saisi dans le fichier apparaîtra mot pour mot dans la sortie. En revanche, les régions dynamiques vous permettent de déterminer par programme la sortie, qui peut dépendre (et dépendra généralement) d'une propriété du modèle.

Par exemple, dans exception.eglje n'utilise qu'une seule propriété : name pour faire varier la sortie. Le request_response.egl modèle est plus complexe, utilisant des boucles for pour déclarer tous les champs de la classe et s'appuyant sur des fonctions d'aide que j'ai définies dans le fichier helper_functions.egl. Puisque EGL est construit sur EOL, les régions dynamiques peuvent contenir n'importe quel code EOL, y compris les opérations. EGL a également des "opérations modèles" (annotées avec @template), qui sont des fonctions qui produisent du texte (ou renvoient le texte sous forme de chaîne) lorsqu'elles sont appelées. Ainsi, nous pouvons construire notre bibliothèque de fonctions utilitaires et les réutiliser à travers les modèles.

Vous vous demandez peut-être d'où viennent ces variables et comment les modèles sont invoqués. Après tout, pour qu'un modèle soit utile, il doit être paramétré avec des valeurs du modèle. Et où les données de sortie sont-elles écrites ? C'est là qu'intervient le principal avantage de l'utilisation d'Epsilon : son langage de coordination EGX. L'idée d'EGX est de fournir un langage basé sur des règles qui contrôle quand et comment les modèles sont appelés, les paramètres avec lesquels ils sont appelés et où la sortie est dirigée. Le specs.egx fichier définit cette logique - c'est ce qui rassemble le modèle et les modèles. La section pre est exécutée en premier et contient principalement des déclarations de variables qui seront utilisées dans le script, telles que le répertoire de sortie et les noms communs. Elle récupère également du modèle différents types et les catégorise par propriétés, comme par exemple s'il s'agit de requêtes ou de réponses (nous avons défini ceci comme une propriété booléenne de Class dans notre métamodèle pour cette raison). Les opérations déclarées sur le type Class dans ce script sont utilisées pour dériver les variables qui seront transmises à chaque modèle à partir des propriétés de l'élément de modèle.

Pour y voir plus clair, prenons un exemple. Prenons la règle règle QueryParamsRequest. Pensez aux éléments transform et in comme une boucle for améliorée. La collection d'entrée,, dans ce cas,, provient de l'élément queryParamsRequestTypes calculée dans pre. Dans d'autres cas (par exemple, la Enum règle), il provient de toutes les instances d'un type donné dans le modèle. Pour chaque élément du modèle (dans notre exemple, lié à la variable request), les parameters sont obtenues à partir d'une méthode utilitaire. Celle-ci lie les noms de variables à utiliser dans le modèle aux propriétés de request - Notez que les opérations utilisées self car l'opération est déclarée sur le type Classqui possède les attributs que nous souhaitons utiliser. Le template est un chemin relatif vers le modèle EGL que nous voulons invoquer pour cette règle. Enfin, le target est le fichier dans lequel nous voulons écrire les résultats. Par défaut, EGX écrase le fichier s'il existe, mais ceci est configurable.

J'espère que cela commence à avoir un sens ! Vous pouvez voir comment, avec EGX, nous pouvons choisir les modèles à invoquer en fonction des types d'éléments du modèle et comment les variables de ces modèles sont dérivées du modèle. Cette logique de coordination est vraiment la pièce maîtresse de l'approche. Pour l'exécuter, il suffit de fournir au modèle le(s) modèle(s) souhaité(s) et d'obtenir la sortie.

C'est tout pour l'instant...

Cet article a brièvement expliqué l'approche de l'EDM adoptée pour aider dans le cas spécifique de la génération d'un gabarit pour le SDK Java. J'espère que la valeur ajoutée de cette approche est évidente, mais vous serez peut-être plus curieux de connaître le processus de développement et les leçons tirées de cet exercice. Dans la troisième et dernière partie de cette série, je réfléchirai à l'approche et fournirai quelques éléments clés à garder à l'esprit si vous décidez de suivre cette voie pour vos projets.

Si vous avez des commentaires ou des suggestions, n'hésitez pas à nous contacter sur X, anciennement connu sous le nom de Twitter ou à notre Slack communautaire. J'espère que cet article vous a été utile et je vous invite à me faire part de vos réflexions/opinions. Si vous l'avez apprécié, jetez un coup d'œil à mes autres articles sur Java.

Partager:

https://a.storyblok.com/f/270183/400x400/46a3751f47/sina-madani.png
Sina MadaniVonage Ancien membre de l'équipe

Sina est développeur Java chez Vonage. Il est issu d'une formation universitaire et est généralement curieux de tout ce qui touche aux voitures, aux ordinateurs, à la programmation, à la technologie et à la nature humaine. Pendant son temps libre, on peut le trouver en train de marcher ou de jouer à des jeux vidéo compétitifs.