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

Augmentez votre productivité grâce à l'ingénierie dirigée par les modèles (Partie 3)

Publié le January 31, 2024

Temps de lecture : 15 minutes

Introduction

Bienvenue à la fin de cette série sur l'ingénierie guidée par les modèles. Dans la partie 2j'ai décrit 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. Dans cet article, je soulignerai quelques principes clés basés sur les leçons apprises afin que vous puissiez tirer le meilleur parti des technologies et de l'approche.

Connaître son "pourquoi"

La première chose à garder à l'esprit avant d'aller plus loin est de s'assurer que cela correspond bien à votre cas d'utilisation. Comme le dit le proverbe, "quand on n'a qu'un marteau, tout ressemble à un clou". Il est naturel que les technologies passent par des modes - c'est ce que l'on appelle le "cycle de hype de Gartner". "cycle de hype de Gartner". Si certaines technologies et méthodologies de développement semblent appétissantes, il ne s'agit en fin de compte que d'outils. Il est bon d'avoir une boîte à outils diversifiée, car cela signifie que nous pouvons choisir l'outil ou l'approche qui convient à la tâche à accomplir. Ce qui convient à un cas d'utilisation peut ne pas convenir à un autre. Alors, quand l'EDM est-il le bon choix et quand ne l'est-il pas ? Voici quelques questions à se poser.

Le domaine est-il facile à modéliser ?

Tous les défis ne s'inscrivent pas dans un métamodèle facilement définissable. Si, dans de nombreux cas, il est possible d'adopter une vision orientée objet d'un domaine avec des relations prévisibles entre les concepts du domaine, il peut arriver que la nature de ces concepts et même la manière dont ils se rapportent les uns aux autres soient trop dynamiques. Pour tirer le meilleur parti d'une approche guidée par les modèles, le métamodèle du domaine doit être au moins définissable avec un degré élevé de certitude. Les concepts doivent correspondre à des classes dont les attributs et les relations sont définissables. S'il y a trop d'éléments de domaine à définir et que les relations sont trop complexes, votre métamodèle en sera le reflet. Dans ce cas, la définition du métamodèle et des modèles sera une tâche trop importante et pourrait dépasser les avantages d'une mise en œuvre directe.

Quelle sera la complexité du générateur ?

Si votre domaine peut être clairement défini à l'aide d'un métamodèle, vous devez également vous demander comment ces concepts s'appliquent au code et à la logique d'entreprise que vous souhaitez générer. Il existe un argument en faveur des deux extrêmes : si la logique est dense (c'est-à-dire qu'elle dépend fortement du modèle), la génération automatique réduit le risque d'erreurs. D'un autre côté, cela peut rendre le générateur plus difficile à maintenir et à utiliser. Il convient également de s'interroger sur le nombre de modèles que vous devrez développer et maintenir, ainsi que sur la complexité globale de la logique de coordination pour l'invocation de ces modèles, en particulier par rapport à un code écrit à la main.

Combien de codifications manuelles cela permettra-t-il d'économiser ?

L'une des questions les plus importantes, et peut-être aussi les plus simples, est celle du rapport entre le modèle et la logique. Une approche de génération de code basée sur un modèle fonctionne très bien lorsque le code généré est principalement un modèle standard, de sorte que la sortie peut être collée dans votre base de code avec un minimum de modifications, voire aucune. Cependant, si le code généré nécessite un codage manuel supplémentaire significatif - en particulier s'il s'agit de modifier le code généré plutôt que d'ajouter de nouvelles méthodes, par exemple - la valeur du générateur diminue. En effet, les frais généraux liés au développement et à la maintenance du générateur, ainsi qu'au remaniement ou à la modification du code généré à la main, peuvent représenter un fardeau pour certains développeurs par rapport à l'écriture de ces sections du code à partir de zéro.

À quelle fréquence l'utiliserez-vous ?

Même si une approche orientée modèle est réalisable - c'est-à-dire si votre domaine s'intègre parfaitement dans un métamodèle et que la génération de code est bénéfique - cela peut ne pas valoir la peine si vous n'envisagez de l'utiliser qu'une seule fois. Bien entendu, cela dépend d'autres facteurs tels que la quantité de code que vous générez - s'il s'agit d'un projet important, l'investissement peut valoir la peine, même s'il s'agit d'une utilisation unique. Dans d'autres cas, les "économies" peuvent être modestes mais s'accumuler au fil du temps grâce à une utilisation fréquente. Une autre façon d'aborder cette question est de se demander "Combien de modèles aurons-nous ?" - il est probable que plus vous créerez de modèles (et plus vous les modifierez fréquemment), plus vous utiliserez le générateur.

Est-il facile de mettre à jour votre (vos) modèle(s) ?

Dans le point précédent, j'ai indiqué que le fait d'avoir plus de modèles ou des modèles qui changent fréquemment vous permet de mieux tirer parti des avantages de l'utilisation d'une approche de génération de code. Cependant, si vos modèles sont construits uniquement pour cet exercice et doivent être constamment mis à jour pour refléter l'évolution des besoins de l'entreprise ou de l'environnement, cela peut ajouter des frictions. Dans mon cas, par exemple, tout changement dans les spécifications de l'API nécessite la mise à jour du modèle. La charge que représente la mise à jour du modèle est directement proportionnelle à l'ampleur des changements apportés à la spécification. Si les modèles doivent évoluer rapidement, cela signifie que tout code généré précédemment est désormais invalide et doit être généré à nouveau. Il faut également tenir compte du fait qu'une partie du code généré peut avoir été (ou doit être) modifiée à la main, comme nous l'avons vu dans un point précédent.

Le métamodèle devra-t-il être modifié fréquemment ?

Les modèles étant des données structurées, il est raisonnable de s'attendre à ce qu'ils changent, parfois même rapidement. C'est très bien ainsi et les frictions sont relativement faibles si les mises à jour du modèle sont automatisées. Ce qui ne devrait pas changer fréquemment, cependant, c'est le schéma (métamodèle). C'est un peu la même chose que pour le premier point : si les concepts de votre domaine changent fréquemment et qu'il est difficile de les rattacher à un métamodèle stable, il est beaucoup plus difficile de suivre une approche guidée par le modèle. Idéalement, le métamodèle devrait être révisé principalement pendant la phase de prototypage et rarement une fois qu'il est stable et "en production" (c'est-à-dire que le résultat est utilisable). L'ajout de champs au métamodèle n'est généralement pas un changement radical puisqu'ils peuvent être optionnels, mais la suppression de classes, de relations et d'attributs peut (et va généralement) briser vos modèles et flux de travail existants, ce qui signifie que vous devez repartir à zéro dans certains cas.

Outre les corrections de bogues, à quelle fréquence le générateur sera-t-il modifié ?

Au cours du développement initial des modèles de générateur de code et de la logique de coordination, il est naturel qu'il y ait de nombreux bogues ou omissions. Une fois que ceux-ci sont en grande partie résolus et que la sortie générée est conforme aux attentes, le générateur peut être considéré comme stable. Cependant, même après cela, il y aura inévitablement des changements dus à de nouvelles exigences commerciales, à de nouveaux guides de style ou à une nouvelle structure de code. Quelle qu'en soit la raison, il convient de considérer que toute modification apportée à un générateur stable risque d'introduire de nouveaux bogues, qui devront à nouveau être éliminés. Comme ceux-ci ne peuvent généralement être détectés qu'en inspectant le code de sortie, l'idéal serait que le générateur n'ait pas à subir de changements fréquents. En effet, le générateur est (ou peut être) plus difficile à tester que le code généré, de sorte que l'évolution rapide des besoins de l'entreprise, qui doit se refléter dans la logique de modélisation, introduit une friction supplémentaire.

Votre équipe est-elle d'accord avec cette démarche ?

Enfin, même si une approche axée sur les modèles semble parfaitement adaptée à votre cas d'utilisation, il est peu probable qu'elle soit couronnée de succès si votre responsable et vos collègues ne sont pas disposés à s'adapter à ce flux de travail. Après tout, la construction du métamodèle, du générateur et des modèles, ainsi que leur maintenance, constituent une charge supplémentaire qui ajoute à la complexité et exige que les gens comprennent les technologies utilisées. Les facteurs organisationnels jouent également un rôle - comme l'origine des modèles, qui les créera ou les mettra à jour et comment, qui est responsable du générateur, quel type de processus sera utilisé pour extraire les résultats générés et les ajouter à la base de code existante, etc. Il y a de nombreux défis logistiques à relever dans le cadre de cette approche, sans parler de la conformité et de l'audit ! Si vous utilisez cette approche pour générer des modèles standard, mais que tous ceux qui l'utilisent promettent d'inspecter et de réviser manuellement le code généré avant de l'intégrer à la base de code principale, il devrait y avoir relativement peu de frictions. En revanche, s'il s'agit d'une partie intégrante et obligatoire du processus de développement automatisé, une réflexion plus approfondie et une diligence raisonnable s'imposent. Dans tous les cas, les personnes qui utilisent les modèles, le générateur ou les résultats obtenus doivent être au courant de l'approche et l'approuver.

Développement itératif

Supposons que vous ayez réfléchi à toutes les questions ci-dessus et que vous ayez déterminé que l'approche est adaptée à votre cas d'utilisation. Comment devez-vous vous y prendre pour la mettre en œuvre ? D'après ce que j'ai présenté jusqu'à présent, vous avez peut-être l'impression que le développement guidé par le modèle est un processus "en cascade". S'il est vrai que le métamodèle doit être stable et constitue naturellement le point de départ, en pratique, il y a plus de flexibilité que la littérature et les outils ne le laissent penser.

Dans mon cas, je n'ai certainement pas commencé avec le métamodèle parfait, et même aujourd'hui, il y a encore beaucoup de place pour l'amélioration. Par exemple, lors de la création de modèles, j'ai constaté que je devais créer des types Java intégrés pour pouvoir les référencer, alors qu'ils pourraient faire partie du métamodèle. De nombreux attributs du modèle - en particulier les attributs booléens tels que isRequest, isResponse, isHal, isQueryParams etc. sont apparus lorsque j'ai réalisé qu'ils seraient nécessaires au générateur. Les changements additifs ne sont pas rédhibitoires - vous pouvez généralement étendre votre métamodèle avec de nouveaux types et attributs tant qu'ils sont optionnels ou qu'ils ont une valeur par défaut sensée.

Le développement guidé par le modèle reste un développement de logiciel, de sorte qu'un processus itératif vous permet d'obtenir un retour d'information plus rapide plutôt que d'investir trop lourdement dès le départ, lorsque vous disposez de moins d'informations. En pratique, je vous recommande donc de développer le métamodèle, le générateur et un échantillon de modèle en parallèle, afin de voir le flux de travail de bout en bout et de repérer les paramètres manquants dans votre métamodèle ainsi que les erreurs dans le texte généré dès le début, plutôt qu'après avoir conçu ce qui, sur le papier, semble être "complet".

"One-Shot" ou MDE "pur" ?

Le cas d'utilisation que j'ai présenté dans la partie 2 était en grande partie une approche "one-shot". En d'autres termes, une fois que le code a été généré, je n'ai plus besoin du générateur ou du modèle. Par exemple, si la spécification de l'API est mise à jour ou si je repère des bogues dans le code de sortie, à moins qu'il n'y ait des problèmes ou des révisions majeurs, il y a de fortes chances que je ne mette pas à jour le modèle et que je ne régénère pas le code parce que j'ai déjà remanié et fait évoluer le code généré. Après tout, c'est le résultat qui m'importe et qui sera maintenu. En ce sens, je ne pratiquais pas vraiment l'EDM comme il est "censé" être fait. Le code généré est un point de départ pour construire, pas le produit fini. D'aucuns pourraient donc affirmer que l'esprit de l'EDM n'est pas guidé par les modèles. Cela ne veut pas dire que ce n'est pas utile.

Dans une approche traditionnelle guidée par le modèle, le code serait idéalement régénéré à partir du modèle à chaque fois que celui-ci est mis à jour, de sorte que la base de code et le modèle soient toujours synchronisés. Pensez-y de la manière suivante : lorsque vous écrivez un programme dans un langage JVM (Java, Kotlin, Groovy, Scala, etc.), qu'est-ce que vous enregistrez dans votre système de contrôle des sources ? Qu'est-ce que vous maintenez ? De quoi vous préoccupez-vous ? C'est le code source. Mais ce n'est pas ce code source qui importe au moment de l'exécution ; c'est le bytecode (c.-à-d. les .class fichiers). Comment se fait-il que nous tenions beaucoup à notre code alors qu'il se compile de toute façon en bytecode Java ? Parce que le bytecode est dérivé de manière déterministe du code source. On pourrait dire que le principal atout de ces langages est le compilateur. En effet, un compilateur est essentiellement un transformateur : votre code source est le modèle (conforme au métamodèle du langage), le compilateur est le générateur, et le bytecode est la sortie du générateur (J'ai déjà écrit à ce sujet si vous êtes curieux). De la même manière, le code généré dans un "MDE pur" devrait être traité de la même manière que le code compilé : nécessaire mais pas quelque chose que nous devons regarder ou dont nous devons nous soucier. Les modèles du générateur sont essentiellement le "code source" de MDE.

Il est important d'y réfléchir en même temps qu'aux questions que j'ai posées précédemment, car le maintien manuel de la synchronisation entre le modèle et le code source est une source de friction et d'erreurs potentielles. Si vous adoptez une approche plus pragmatique où vous souhaitez obtenir un code initial et ne prévoyez pas de réutiliser le générateur avec le même modèle, alors ce problème est moins important. De plus, supposons que le code généré ne nécessite pas beaucoup (ou pas du tout) de modifications manuelles. Dans ce cas, vous pouvez mettre à jour le modèle ou le générateur lorsque vous repérez des erreurs dans le code de sortie et le régénérer avec un minimum d'effort manuel. D'un autre côté, si le code de sortie doit être modifié de manière substantielle à la main après la génération, il n'est probablement pas utile de mettre à jour le modèle et de le régénérer, car vous devez à nouveau réconcilier les modifications manuelles avec le code généré.

Les ordures entrent, les ordures sortent

Cela m'amène au point suivant. On ne retire de cette approche que ce que l'on y met. Ceci est vrai à la fois pour le(s) modèle(s) et le générateur. Si vous prenez des raccourcis lors de la construction de votre modèle en laissant des champs non essentiels vides, le code généré ne pourra pas avoir la sortie souhaitée. Dans mon exemple, j'ai parfois été paresseux en ce qui concerne la documentation et les valeurs utilisées pour les tests, car je me disais que j'aurais de toute façon besoin de les modifier. Mais lorsque j'ai investi plus d'efforts, j'ai constaté que j'avais besoin de moins de modifications. Comme indiqué précédemment, vous pouvez toujours compléter ou modifier les modèles de votre générateur si vous constatez que des fonctionnalités supplémentaires sont nécessaires ou qu'il y a des erreurs de formatage. En prenant le temps de vous assurer que votre modèle est correct et aussi complet que possible par rapport à vos besoins, vous réduirez le nombre d'erreurs et la nécessité de réexécuter le générateur. Cela est particulièrement important dans les approches ponctuelles ou lorsque le code généré est modifié manuellement après la génération initiale.

Le principe de Pareto s'applique

La règle des règle des 80/20 s'applique tout à fait à la génération de code pilotée par le modèle, selon mon expérience, et renforce les arguments en faveur du développement itératif. Vous pouvez obtenir environ 80 % des avantages avec 20 % des efforts. Si vous commencez avec des modèles simples où vous générez ne serait-ce que le squelette de base des classes dont vous aurez besoin, vous avez déjà écrit une bonne partie du code. Vous avez les classes avec les noms corrects dans le bon paquetage, avec l'en-tête de copyright, les importations typiques, les méthodes, les déclarations de champs, les constructeurs, etc. Vous pouvez ensuite éliminer progressivement la logique plus complexe écrite à la main si vous la trouvez répétitive dans le générateur.

Commencez toujours par les tâches les plus simples et n'essayez pas d'en faire trop à la fois. J'ai trouvé que c'était particulièrement le cas pour la génération du code de test : écrire un générateur pour créer simplement les classes de test et les méthodes avec les assertions m'a fait gagner beaucoup de temps. Je pouvais alors me concentrer sur l'écriture manuelle de la logique de test essentielle sans me préoccuper de la pléthore. Cela ajoute également une structure à votre flux de travail, de sorte que vous sachiez exactement ce qui doit être fait sans être submergé par la nécessité de traiter toutes les petites choses. Vous seriez surpris de voir à quel point cela peut stimuler votre motivation et votre productivité !

D'un autre côté, il est important de trouver un équilibre avec les aspects pratiques de la maintenance et de l'évolution de la base de code. Si la maintenance du code généré vous importe plus que celle du générateur et des modèles, ne sur-ingénieriez pas votre générateur car vous finirez par le rendre exponentiellement plus complexe pour des bénéfices très marginaux. Le générateur devient alors plus intimidant et difficile à utiliser, et l'intégration d'autres personnes devient un défi. En règle générale, essayez de faire en sorte que chaque modèle de générateur ressemble à la sortie générée. Supposons que vos modèles se composent davantage de sections dynamiques avec une logique de branchement importante que de texte statique. Dans ce cas, il est probablement utile de diviser cette logique en méthodes utilitaires ou de simplifier le modèle pour le rendre plus lisible.

Quoi qu'il en soit, il convient de garder à l'esprit qu'il n'y a pas de "magie" ici : la création et la maintenance du flux de travail de l'EDM, ainsi que de ses outils, constituent des frais généraux supplémentaires. Soyez conscient du temps que vous y consacrez par rapport aux économies que vous réalisez. Et cela nous ramène à la justification de la raison d'être de cette démarche en premier lieu.

Conclusion

Nous voici arrivés à la fin de cette mini-série de blogs sur l'ingénierie pragmatique pilotée par les modèles. J'espère que vous avez non seulement appris de nouveaux outils et une nouvelle approche de développement, mais que vous êtes également conscient de ses limites et de la façon de tirer le meilleur parti de ces technologies. Enfin, j'espère que cette introduction à l'ingénierie dirigée par les modèles et cette étude de cas serviront d'exemple utile et vous aideront à prendre de meilleures décisions dans vos projets logiciels, si vous décidez de suivre cette voie.

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.