samedi 7 juillet 2012

Retour d'expérience : bonnes pratiques d'une équipe d'architecture logicielle

Je termine une mission d'architecture logicielle dans une cellule Méthodologie de Développement et MDA dans une grande DSI française. Au sein de cette équipe, nous définissons et mettons en place des architectures logicielles pour les projets.
Le travail s'axe autour de plusieurs aspects :
  • Formation et accompagnement des équipes projets sur les phases de conception (modélisation, Design Pattern), développement, déploiement.
  • Conception d'outillage pour la démarche MDA : langage de modélisation interne, outils de modélisation et générateur de code;
  • Réalisation et mise à disposition des outils du poste de développeur.
Le tout dans un contexte de méthodologie agile.
Je voulais vous remonter mon retour d'expérience pour partager les bonnes pratiques qui ont fait le succès de cette approche.

Faciliter le passage de connaissance pour consolider l'équipe

Afin de renforcer l'esprit d'équipe, les membres de l'équipe "tournent" sur le support au développement. Forcément, en pratique, certains auront certaines affinités sur des sujets selon leurs propres expériences (on avait le "specialiste performance", le "specialiste sécurité", etc), mais on essaie toujours de partager cette connaissance.
On consolide l'équipe avec cette approche. En cas d'indisponibilité, chaque membre de l'équipe peut reprendre le travail de l'autre rapidement. On est également plus réactif, car si un membre n'est pas disponible, on ne se retrouve pas bloqué à l'attendre. Mais surtout, chaque membre monte en compétence sur les différents sujet Même chose sur la méthodologie agile : A chaque sprint, le rôle de Scrum master tourne.
Pour faciliter les passages de connaissances entre les membres de l'équipe un blog permet de poster des solutions de certaines problématiques techniques qui pourront servir pour les autres membres plus tard. Lorsque le blog n'est pas suffisant, et que nous avons des besoins d’interaction, nous faisons un point technique "démo/tutoriel" avec le vidéoprojecteur.

Faciliter le passage de connaissance aux équipes projets

"Quand un homme à faim, mieux vaut lui apprendre à pêcher que de lui donner du poisson" disait Confucius.
C'est dans cet état d'esprit que nous faisons le support aux développeurs. Nous privilégions le passage de connaissance afin que les développeurs soient autonomes et montent en compétences, plutôt que de corriger leurs problèmes sans explications.
Ce passage de connaissance se fait de plusieurs manières : Un blog et une FAQ avec des articles expliquants les cas les plus récurrents (mise en place de la sécurité par exemple), du pair-programming avec les développeurs et même des screencasts.

Proximité de l'équipe

L'équipe au complet est localisée dans un même bureau. Quand le Product Owner, le Scrum master et les membres de l'équipe sont dans un même espace de travail, on favorise les échanges et on facilite la communication tous les membres de l'équipe. Les problèmes sont remontés au plus tôt et on sait plus facilement sur quoi travaille chacun de nous.
Pour aller encore plus loin dans l'échange et le suivi, nous avons - en tant que bons agilistes - notre stand-up meeting quotidien (limité à 15 minutes pour l'équipe, attention !), où chacun revient sur ces 3 questions "Qu'ai-je fait hier", "Sur quoi ai-je été bloqué" et "Que fais-je aujourd'hui". En essayant de ne pas s'égarer...
La proximité est également "virtuelle" Nous partageons des outils de travail : boite mail commune à l'équipe, outil de suivi commun à l'équipe.

Capitaliser sur les outils pour développer durablement

Lors de nouveaux besoins des équipes projets, certains outils spécifiques ont été développés. Il a rarement été question d'outillage "jetable". Chaque outil est développé selon les besoins actuels, puis remonté dans le référentiels des outils. Si un nouveau besoin se fait ressentir, alors on fait des évolutions sur l'outil.
Cette approche présente plusieurs intérêts :
- Lors d'un nouveau besoin : on se limite au cas d'utilisation actuel en ne développant que les fonctionnalités utiles au moment T.
- Lors de la réutilisation : on ne repart pas de zéro : on réutilise l'outil existant dans le référentiel
- Lorsque de nouveaux besoins se font ressentir : on fait les évolutions mais seulement sur le nouveau cas d'utilisation, en refactorant si besoin le code existant.
On garde une approche incrémentale, en se focalisant seulement sur les besoins en cours et en ne surdimensionnant pas les outils (on reste agiles).

Appliquer à soi-même les conseils que l'on prodigue aux autres

Nous poussons les équipes projets à avoir une approche TDD et d'intégration continue. Nous appliquons à nous-même ces préconisations. Notre outillage suit une approche TDD et il est placé dans une chaîne d'intégration continue afin de valider au plus tôt les problèmes.
Nous avons mis en place cette approche car nous nous sommes rendu compte que lors d'évolutions ou de corrections de bug sur notre outillage, nous pouvions amener des régressions. Même si nous sommes une équipe d'architectes logiciels, nous sommes avant tout des développeurs et à ce titre, nous devions également suivre les même processus de développement que les projets "métier" afin d'assurer un outillage de qualité.

Se remettre en question pour s'améliorer

Nous mettons un point d'honneur à l'amélioration continue grâce, entre autre, aux rétrospectives de Sprint.
Tout n'est pas parfait et nous l'acceptons. D'ailleurs, je pense que c'est ce qui fait que ça fonctionne bien : accepter que tout n'est pas parfait, permet de s'améliorer. Lors de la rétrospective de Sprint, nous remontons donc tout ce qui ne va pas au niveau organisationnel, humain ou technique et on tente de mettre en place des solutions pour améliorer ces points lors itérations suivantes.
L'autre point d'amélioration continue, ce sont les retours d'expérience auprès des équipes projets. Lorsque les projets sont terminées (ou en phase avancées de développement), nous faisons un point avec le lead developer, le responsable de l'équipe, l'architecte. et nous prenons en  compte les remarques et critiques sur notre outillage, nos formations, notre support. Nous prenons en compte également les idées qui permettraient d'améliorer le travail des développeurs.

En conclusion

Toutes ces bonnes pratiques nous ont permis de rester ouverts sur les développeurs et de nous améliorer en continu et surtout d'éviter le syndrome de la tour d'ivoire.
Peut-être utilisez vous d'autres bonnes pratiques de développement ("Eat your own dog food" par exemple) ou d'agilité (Kanban) ou d'apprentissage (Shu ha ri)
N'hésitez pas à nous remonter vos retours d'expériences en interne ou en mission pour que l'on puisse partager sur ces sujets.

lundi 23 avril 2012

Agilité : Améliorer son planning poker


A Devoxx France, Petra Cross nous a donné sont point de vue sur l'agilité telle qu'elle est utilisée chez Google dans son talk "Day to day software development at Google" pour être efficace et éviter de perdre trop de temps. Je trouve interessant de poster à ce sujet, car nous avons toujours du mal a estimer les user stories de manière abstraite et avons tendance à pendre trop de temps dans la négociation (et s'égarer dans les explications). Petra nous donne des astuces intéressantes pour mieux mettre en place son planning poker.

Comment s’abstraire du temps dans la macro-estimation des user stories ?
L’estimation des stories se fait normalement de manière abstraite. On parle de macro-estimation en point. Je m’explique. L’estimation des stories ne doit pas être faite avec la notion de temps (durée prévue pour la réalisation en jours/homme) mais avec une notion de points sans unité.

Pourquoi cette abstraction ? Car elle permet de mieux estimer une story et éviter les écarts de temps en fin d’itération. En voulant macro-estimer en j*h, on va faire en sorte que la somme des estimations de tâches corresponde avec l'estimation de la story. Or, le résultat est forcément biaisé, car la macro-estimation suit une suite de Fibonacci (1, 2, 3, 5, 8…) et ce qui va induire des décalages dans les estimations qui, elles, ne sont suivent pas cette suite. 

Alors comment bien macro-estimer ?
Il faut pouvoir juger de la complexité d’une story et non pas de son temps de réalisation prévu. L’idée est de définir, avant le début du premier sprint (et donc avant les premières estimations) 2 étalons sur des stories déjà réalisées par l’équipe et dont tous les membres maîtrisent la complexité. Tous les membres de l’équipe doivent se mettre d’accord pour définir ce qu’est :
  • un story A de 2 points
  • un story B de 5 points

par exemple
  • la story « implémentation de page d’information utilisateur » est estimée à 2 points;
  • la story « implémentation du tableau d’affichage des indicateurs » est estimée à 5 points

Avoir un seul étalon de 1 point n’est pas suffisant car il est difficile de savoir ce que représenteront des stories plus grandes. Avoir 2 stories étalons va permettre de mieux estimer car il est plus facile de faire des comparaisons entre plusieurs valeurs.

par exemple
  • Je considère que cette story X est plus simple à réaliser que la story étalon A => alors elle est estimée à 1 point
  • Je considère qu’elle est plus complexe que l'étalon A, mais plus simple que l'étalon B => alors elle est estimée à 3 points.
  • Je considère qu’elle est plus complexe que l'étalon B => Alors elle est estimée à 8 points.

Une fonctionnalité ne doit pas être estimée à plus de 8 points. Si les membres de l'équipe estiment la story comme ayant plus de 8 points alors il faut découper la story en plusieurs moins complexes.

Petra propose également un point de vue intéressant sur l'étape de négociation des valeurs estimées. La négociation entre les membres ne se fait que si les estimations ont un écart important (sur l'échelle de fibonacci). Si l'écart est minime, alors, c'est la majorité qui l'emporte.

Par exemple, sur une équipe avec 3 membres Paul, Bill, John : 
Si une story est estimée à 2 par Paul, 2 par Bill et 3 par John (je note 2-2-3) => l'écart entre l'estimation de John (3) et les estimations de Paul et Bill (2) étant minime, alors on considère l'estimation finale à 2 sans négociation des membres. l'estimation 2 étant majoritaire. 
idem, si on a le cas 3-3-2 => l'estimation sera alors de 3.
Si on a 3-3-8 (ou 1-1-3), alors on négocie car l'écart est important. Mais, pour éviter que la négociation soit trop longue, les membres de l'équipes vont d'abord poser des questions fermées permettant de mieux comprendre le périmètre la story avant de se mettre d'accord sur une estimation finale.

par exemple, "Est-ce que le bouton valider doit être implémenté dans cette story ?", "Doit on prendre en compte les calculs de taux dans cette story ?"

S'il n'y toujours pas consensus à la suite des questions fermées, alors, on pourra envisager des négociations ouvertes.

La prise en compte de points abstraits n'étant pas naturelle  (puisque sans unité) son utilisation ne sera pas forcément efficace dès les premiers sprints. Mais normalement, après 3-4 sprints, tous les membres devraient mieux appréhender son utilisation.

Il ne reste plus qu'à essayer de mettre en place cette solution. Je reste ouvert si vous avez d'autres solutions / façons de faire !


mardi 7 février 2012

Les limites du MDA en entreprise

Je souhaitais faire un retour d'expérience concret sur les cas "typiques" de limites de l'approche MDA en entreprise d'un point de vue "développeur". Ces limites peuvent pousser, dans certains cas, à abandonner l'approche MDA.
Ce retour d'expérience personnel est issu de plusieurs points de vue : de mon œil neuf que j'avais lorsque j'ai débuté et que j'étais "utilisateur du générateur", puis d'un œil plus expérimenté en tant que "concepteur de générateur" et en condensant les retours d'expériences des développeurs au sein de différents projets utilisant l'approche MDA.

Une idée du contexte : les DSL ne sont pas forcément Turing-complet mais, dans tous les cas, le générateur mixe code manuel et code généré (approche par balises comme défini dans l'article précédent). Les DSL sont dans tous les cas des modèles (je n'ai pas de retour d'expérience sur des DSL texuels). Ils sont indépendants ou basés sur une extension d'UML avec des profils.

Les cas limites du MDA

L'inexpérience du développeur
Le développeur débutant ne connait pas les enjeux liés à l'approche MDA.
Il développe en dehors des balises prévues à cet effet et remet en cause l'outillage MDA de lui avoir fait perdre tout son travail.
Il considère que le code généré est trop compliqué à lire et à modifier et préfère considérer sa propre approche.
La phase de conception peut également être problématique pour les développeurs qui veulent tout de suite "coder du technique". Le cas le plus typique étant les développeurs avec une vision bottom-up et qui souhaite modéliser la base de données sans vision fonctionnelle ( "Comment ça je ne peux pas modéliser une colonne de table du type CHAR(3) pour Oracle ?" ).
Finalement, il laisse tomber l'approche MDA.

Le temps  (ou la paresse du développeur...)
Le développeur connait les intérêts de l'approche MDA, mais considère le round-trip trop consommateur en temps et préfère modifier le code sans passer par le modèle.  En fin de projet, les délais sont serrés et le modèle devient moins prioritaire : seule la livraison compte !

La raison est simple : ajouter du code depuis l'IDE lui prend très peu de temps comparé au temps de round-trip : modélisation, export XMI, validation du modèle, génération, vérification de la validité du code. Sans compter le temps d'ouverture des outils, le temps de chargement (du modeleur et du générateur) qui s’avèrent non négligeable. La fréquence de round-trip s'avère donc incompatible avec des délais courts.
De plus, la plupart du temps, le modèle ne fait pas partie de la livraison. Refaire une étape de modélisation n'est donc pas jugée prioritaire.

En solution, le développeur ajoute un maximum de code manuel alors que ce code pourrait être intégré au modèle. Finalement, il se rend compte que le modèle n'est plus vraiment à jour, et que la resynchronisation modèle / code sera coûteux et ne sera donc fait qu'"après la livraison, quand on aura plus de temps". Traduisez par "Jamais".

La perte de code
Le développeur effectue des roundtrips réguliers, mais il s'aperçoit que certaines portions de code ne sont pas correctement regénérées et lui font perdre son code manuel.
Ce cas peut arriver pour plusieurs raisons :

  • Les balises de code manuel peuvent être supprimées lors d'une réorganisation automatique du code par l'IDE.
    Par exemple, les balises autour des zones d'import de fichier Java sont souvent effacées lors d'une "réorganisation des imports" sous Eclipse. Sous FlashBuilder 4, les zones d'imports des AS3 sont systématiquement supprimées.
  • Les balises peuvent être modifiées lors d'un reformatage.
    Par exemple, Eclipse formate les commentaires : les balises (qui sont des commentaires) sont modifiées et ne sont donc plus valides. 
  • Une maladresse du développeur, une balise est modifiée et le code manuel n'est pas ré-intégré.
  • Un refactoring qui n'est pas pris en compte par l'outillage : Comment gérer les refactoring de méthodes, de classes ?
    Par exemple, les balises de code manuel pour les méthodes sont constituées de la signature de la méthode. En cas de changement de celle-ci, la balise est changée et le code perdu.
    Autre exemple, pour les renommages de classes : Un classe Toto devient un fichier Toto.java. Lors d'un renommage de Toto en Titi, le code de la classe existante Toto.java n'est pas ré-intégré dans Titi.java. Titi.java est vierge de tout code manuel. Toto.java est toujours présent mais orphelin.
  • Certains fichiers entièrement générés ont besoin d'être customisés par le développeur. Le développeur modifie donc le code généré consciemment. Lors d'une regénération, le code sera perdu, mais le développeur récupérera les modifications dans le SCM et les ré-intégrera manuellement.  
Le temps passé sur le SCM à réintégrer le code perdu devient donc problématique. D'autant plus, lorsque nous préconisons une approche incrémentale, le développeur fera nécessairement plusieurs round-trip au cours du développement de son logiciel. Chaque round-trip nécessitant de la part du développeur qu'il vérifie que du code n'as pas été perdu. La fréquence de round-trip s'avère donc incompatible avec des délais courts (point précédent).
A ajouter également, le non déterminisme du générateur :  Lors de génération successive, pour un même modèle, le code généré n'est pas dans le même ordre. Ce principe s'explique car la spécification du méta-modèle (UML en l’occurrence) n'ordonne pas certaines relations entre objets.

Là encore, il pourra remettre en cause l'approche MDA (notamment sur la fin des projets) comme étant problématique.

La conformité du code
Le développeur perd du temps à corriger des erreurs dans le code généré : erreur de compilation, erreur à l’exécution ou encore pertes de performances.
Suite à un audit de code, ou une validation de qualité, il est démontré que le code généré est la cause de perte de qualité du projet. Le code généré entraine donc une dette technique qu'il faudra prendre en compte.
Plusieurs cas peuvent être remontés :
  • Le code généré ne compile pas ou contient des portions de code au comportement douteux provoquant des bugs;
  • Le code généré fait baisser la note qualitative du code source de l'application. Il n'est pas formaté correctement, n'est pas commenté, il est difficilement lisible et donc difficilement maintenable.
Le développeur perd du temps à remettre le code en conformité.

Ces points à eux seuls ne sont en général pas des causes d'abandon du MDA, mais peuvent faire baisser l'image de cette approche et peuvent avoir un effet cumulatifs avec d'autres points remontés précédemment.


Comment résoudre ces cas ?
Suite à ces différents cas d'abandon, j'ai défini 9 bonnes pratiques à suivre pour résoudre ces cas.
  1. Former les développeurs et les chefs de projets pour leur faire comprendre les intérêts qu'ils vont trouver à l'approche MDA : démarrage du projet plus rapide, suivi des préconisations d'architecture logicielle concernant la stack logicielle de l'entreprise, qualité de code, fiabilité du code technique qui a été éprouvée par les architectes logiciels et dont le développeur n'a plus à se préoccuper.
  2. Limiter le modeleur au DSL. Dans le cas où le DSL est une extension d'un langage existant (UML par exemple), il est nécessaire de customiser le modeleur pour limiter les possibilités de modélisation aux cas restreint par le DSL. Dans la mesure où on peut "ajouter" des concepts à UML, mais pas en "enlever", le modeleur ne pourra pas limiter toutes les possibilités offertes par le langage existant, ce qui nous amène au point suivant...  
  3. Le modèle doit être validé, au moins en partie, pendant la phase de modélisation. Au même titre qu'un code qui ne compile pas, apparait en erreur dans l'IDE, il faut qu'un modèle qui ne soit pas valide apparaisse en erreur dans le modeleur. Ceci implique que la validation de la cohérence d'un modèle doit être faite au plus tôt dans la chaine de génération.
  4. La qualité du modèle doit être remontée au plus tôt. Au même titre que le code peut avoir une note de qualité (par exemple dans Sonar), il doit être envisager d'avoir une note de qualité du modèle. L'approche MDA permet une meilleure qualité de code, mais n'empêche pas de modéliser n'importe quoi (tout en respectant les règles du DSL déjà validées par le point 4). Noter la qualité du modèle permet donc d'assurer une qualité de code accrue. Ce point nécessite également de mettre en place un document de convention de modélisation.
  5. La chaîne de génération doit être transparente : la plus rapide et la moins intrusive possible pour le développeur. Idéalement le développeur devrait modéliser et développer de manière fluide en passant de son modeleur à son IDE de manière instantanée : L'étape de génération devrait lui être cachée !
  6. Le code généré doit être correct (il ne comporte pas d'erreur de syntaxe), efficace (le code généré n'implique pas de pertes de performances et n'ajoute pas de bug supplémentaires à l'application), fonctionnel (Il a été validé par des architectes logiciels par un POC) et maintenable (il répond aux standards de qualité définis dans les conventions de codage).
  7. Le générateur doit être déterministe : il doit toujours générer le même code (et surtout dans le même ordre) pour un même modèle en entrée.
  8. Le générateur, par son action (lors d'une regénération) ou son inaction (modification du code par l'IDE ou erreur du développeur), ne doit pas faire perdre de code manuel.
  9. Lors de la création du générateur, une étude importante doit être faite pour bien définir les zones de codes manuelles et les zones de code entièrement générées. Il faut pouvoir laisser des portes de sorties au développeur pour customiser le code et qu'il ne se sente pas restreint et  considère l'approche MDA comme étant limitative au développement.
Aller plus loin
Ces bonnes pratiques sont bien évidemment spécifiques à ma propre expérience d'un point de vue développeur sur le cas concrets des générateurs mixtes généré/manuel. Dans la suite des articles, j'expliquerai comment on peut faire en sorte de respecter ces bonnes pratiques.


Le point de vue concepteur de générateur apporte également tout un lot de bonnes pratiques à mettre ne œuvre pour que l'approche MDA soit un succès.
L'utilisation d'autres types de générateurs ou de DSL (DSL textuels par exemple) apportent également leurs lot de bonnes pratiques.





jeudi 26 janvier 2012

Internationaliser son DSL sous MagicDraw

Sous Magic Draw, avec un méta-modèle basé sur un profil UML, il est possible de customiser l'interface pour changer le libellé des éléments du DSL.

Cette possibilité offre l'intérêt de pouvoir internationaliser un DSL. On peut donc, pour des raisons techniques (ou de conventions par exemple) définir son DSL ( les stéréotypes et les propriétés de stéréotypes) en anglais et en avoir une représentation en français dans le modeleur (MagicDraw dans le cas présent).

Libellés sur les stéréotypes
Lorsqu'on crée un stéréotype et qu'on l'applique sur une classe, par défaut, cette classe va être représentée comme étant une Class <<stereotype>>.
Par exemple, si je crée un stéréotype <<DomainObject>> et que je l'applique sur ma classe "Product". Et si je crée un stéréotype <<IdDomainAttribute>> que j'applique sur l'attribut de ma classe "Product",  je vais obtenir ceci :  
Dans les spécifications, on voit qu'il s'agit d'une classe.

Il est possible de personnaliser le DSL pour que les éléments ne soit plus affichées comme des classes mais comme des éléments propre du DSL.
Voici les étapes à suivre :
  • Créer une nouvelle Classe DomainObjectCustomization (ici le nom a peu d'importance, mais par convention, il est préférable de la suffixer par Customization)
  • Appliquer un stéréotype <<Customization>> sur cette classe
  • Editer les propriétés de la classe DomainObjectCustomization, puis renseigner "Hide Metatype" à true.
  • Renseigner Customization Target à DomainObject
  • Renseigner le Representation Text à "Domain".
  • Renseigner le keyword à "Domain"
Faire la même chose pour personnaliser l'attribut :  
  • Créer une nouvelle Classe IdDomainAttributeCustomization
  • Appliquer un stéréotype <<Customization>> sur cette classe
  • Editer les propriétés de la classe IdDomainAttributeCustomization, puis renseigner Hide Metatype à true.
  • Renseigner Customization Target à "IdDomainAttribute"
  • Renseigner le Representation Text à "ID".
  • Renseigner le "keyword" à "ID"


En redémarrant (ou en rechargeant le modèle), on obtiendra les changement suivants :
Dans les spécifications, on voit maintenant apparaître Product comme étant un Domain et non plus comme une classe.


Pour info,
Hide Metatype permet de ne plus considérer Product comme une Class <<Domain>> mais comme un Domain.
Representation Text, comme son nom l'indique, permet de changer le texte de représentation du stéréotype. Une sorte d'alias qui sera utilisé dans la fenêtre des spécifications par exemple. Sauf que cette valeur n'est pas prise en compte dans le diagramme.
Keyword, comme son nom ne l'indique pas (du tout), permet de changer le texte de représentation du stéréotype dans le diagramme.




Libellés des propriétés de stéréotype


Dans l'exemple ci dessous, je définie une propriété du stéréotype "ServiceInterface" appelé "secured". Sans customisation, quand j'édite les propriétés sur une classe stéréotypée "ServiceInterface", le libellé a le même nom que celui indiqué dans le profil.
Mais il est possible de customiser le libellé, pour, par exemple, l'afficher en français.
 Les étapes à suivre pour mettre en place cette customisation :

  • Ouvrir la customisation du profile pour l'élément concerné. Ici j'édite la customisation de "ServiceInterface" que j'ai nommé "ServiceInterfaceCustomization" :


  •  clic droit dessus, puis "Nouvel Élément" -> "Propriété"
  •   Le nom de la propriété doit avoir le même nom que la propriété du stéréotype. Dans mon exemple, la propriété s'appelle "secured".
  • Éditer les valeurs de customisation de la propriété nouvellement créée, puis appliquer un stéréotype <<metaProperty>>
  • Indiquer dans la valeur "New name" le nom qui sera dorénavant affiché pour cette propriété.

 Pour info, on peut également jeter un œil sur la présentation d'Andrius Strazdauskas, qui exposait, au MD-Day 2011, les avantages de créer son DSL en se basant sur UML et ses profils (et donc d'utiliser Magic Draw pour la customisation graphique du DSL).

Pour aller plus loin, la documentation officielle de Magic Draw sur le profiling et les DSL

dimanche 22 janvier 2012

Gestion de code mixte généré / manuel

Ce message fait suite à l'explication sur les principes de base des générateurs et s'attarde principalement sur le cas 2 qui était problématique : Comment gérer le code généré et le code manuel pour un même artefact?
J'utilise le terme « artefact » parce que le cas 2 expliqué dans le message précédent ne génère pas obligatoirement un seul fichier comme on va le voir plus bas.

Il existe plusieurs possibilités de gérer les artefacts qui doivent contenir du code généré et du code manuel. Je vais passer en revue les différentes possibilités avec leurs avantages et inconvénients.
Dans ce message (comme dans le précédent d'ailleurs), je représente en bleu le code généré et en jaune le code manuel.


1. L'approche « un fichier par artefact »
1.1. Approche par « balises »
Le code généré contient des balises qui permettent de repérer les zones de code généré et les zones de code manuel. Afin de ne pas affecter la syntaxe du code source, ces balises sont toujours placées en tant que commentaire.
Cette approche n'est donc pas à proprement parlé une approche « génération textuelle », car elle suppose, pour fonctionner, que le fichier généré suive une grammaire qui offre le concept de commentaire. En fait, la portée est très large car quasiment toutes les représentations basées sur du texte proposent des commentaires : Tous les langages (Java, .NET, assembleur, etc.), les langages dédiés (ceux basés sur SGML, SQL, JSON), fichiers de propriétés (properties), les scripts (bash, shell), les documents (RTF, LATeX, PDF). Bref, pas de quoi s'inquiéter sur cet aspect.
Cette solution reste la plus générique et la plus adaptable, quelque soit le type de fichier.

Les balises de code manuel permettent de délimiter les portions de code que le développeur peut modifier sans risque. La portion de code manuel ne sera pas écrasée lors d'une future génération.
Lors d'une regénération, le générateur fait un comparatif entre les balises du fichier qu'il vient de générer et ces mêmes balises dans le fichier existant. Il est ainsi capable de ré-intégrer les portions de code manuel.
Les balises sont des clés et il est important qu'elles restent identiques entre chaque génération. 
La subtilité consiste donc à créer des balises qui soient uniques pour chaque portion de code manuel. Lors de la création du générateur, il faut définir les différentes zones de code manuel et leur associer des balises. Bien sûr, les balises peuvent elles-mêmes être créées dynamiquement pour être constituées avec des informations du modèle.

Exemple
public float calculateTotalPrice(float price, float tax) {
// start of user code
    return price * tax ;
// end of user code 
}


On peut considérer 2 cas :
  • Soit on place des balises sur les portions de code généré, 
  • soit on place des balises sur les portions de code manuel. 
 Tout dépend de l'approche que l'on a. L'idée est évidemment de placer un minimum de balises et donc :
  • Si on génère une grande partie du fichier et que le code manuel est minoritaire => on privilégie une approche « priorité code généré »
  • Si on ne génère qu'une petite partie du fichier et que le code généré est minoritaire => on privilégie une approche « priorité code manuel »

1.1.1. Les balises « priorité au code généré »
Petit aparté : Dans certains outils de génération, on trouve la notion de « balise utilisateur », « user tag » ou « user code ». Je trouve que le terme n'est pas approprié car la notion d'utilisateur me fait penser à l'utilisateur final du logiciel. Or ce n'est pas le cas ici, le code indiqué entre balise est du code « développeur », c'est à dire du code que le développeur doit ajouter manuellement pour que le programme soit fonctionnel. Pour simplifier les choses, il est préférable de parler de « code manuel ».

Ici, le code généré est prioritaire, ce sont donc les portions de code manuel qui sont délimitées. Lors d'une regénération, si les balises sont manquantes, c'est le code manuel qui sera perdu.


Utilisé par : outils de génération MIA Generation ou Acceleo.

1.1.2. Les balises « priorité au code manuel »


Ici, le code manuel est prioritaire et ce sont donc les portions de code générées qui sont délimitées.
Lors d'une regénération, si les balises sont manquantes, le code manuel restera intact.
C'est le principe offert par les wizards et générateurs d'interface qui vont insérer du code généré dans un code manuel.
Utilisé par : NetBeans pour la génération des UI Java Swing.
Exemple de portion de code généré pour l'interface graphique sous NetBeans.


1.1.3. Les balises « depuis » et « jusqu'à ».
Il existe des alternatives proposées par certains outils qui permettent de placer le code généré en début fichier et le code manuel en fin de fichier (ou inversement).
Je les nomme balises « Jusqu'à » et « Depuis ».

Utilisé par : MIA Generation propose cette fonctionnalité au travers des balises code manuel :
  • Si la balise de début n'existe pas et que seul la balise de fin existe, alors tout le début du fichier jusqu'à la balise sera du code manuel. Le fin du fichier sera du code généré.
  • Si la balise de fin n'existe pas et que seul la balise de début existe, alors le code manuel est situé depuis la balise et jusqu'à la fin du fin. Le début du fichier sera du code généré.
Remarque : Malgré la possibilité offerte, je n'ai jamais rencontré le besoin de mettre en place cette approche. Je ne pense pas que cette approche apporte un réel intérêt.

1.1.4. Limitations
  • Cette approche pollue le code avec des balises qui n'ont finalement aucune valeur ajoutée pour le développeur. Heureusement, certains outils (NetBeans par exemple) sont capable de réduire les zones générées dans l'éditeur et donc de ne pas les faire apparaître au développeur.
  • Les risques d'écrasement sont réels. Si le développeur supprime ou modifie une balise utilisateur, le code peut se trouver écrasé par inadvertance. Il existe également des cas pour lesquels la regénération est problématique : le changement de signature de méthode (issu d'un renommage par exemple) modifie la clé de la balise et écrase donc le code manuel existant. Il est nécessaire de coupler le code source avec un SCM pour éviter toute perte de code.


1.2. Approche différentielle
Il s'agit d'une approche plus sophistiquée qui consiste à repérer les différences entre le code généré à vide et le code actuel afin d'en déduire le code manuel.

L'opération consiste ensuite à ré-intégrer ce code manuel dans le fichier nouvellement généré. Contrairement à l'approche par balise, cette approche à une portée plus large : elle ne cible plus spécifiquement les fichiers dont la grammaire propose le concept de commentaires. Du fait de son approche différentielle, elle cible tous les types de fichiers texte.

Utilisé par : Il n'existe pas, à ma connaissance, de générateur qui prennent en compte cette approche (même si John Vlissides y fait référence ici).

Limitations
  • Il s'agit d'une approche risquée car parfois le code manuel peut difficilement être réintégré.
  • Elle est évidemment complexe à mettre en œuvre.
1.3. Approches spécifiques aux langages
Certains langages disposent de métadonnées permettant d'annoter des portions de code comme étant générées.

1.3.1. Java 6 et l'annotation @Generated
A ma connaissance, seul Java 6 propose cette possibilité avec l'annotation @Generated. Elle peut être placé sur une classe, un attribut ou une méthode.

@Generated(value = "ClassNameThatGeneratedThisCode")

    public void toolGeneratedCode(){


    }

Limitations :
  • Le générateur ne doit plus simplement faire une analyse textuel du code source, mais également un parsing de l'AST pour repérer l'annotation @Generated dans le code.
  • Le gestion de la regénération devient donc plus couteuse à mettre en place.
  • De plus, cette approche n'offre pas de souplesse : on ne peut pas délimiter finement les portions de code manuelles.

2. Approche multi-fichiers par artefact

2.1. L'approche « abstraction »
Cette approche s'applique aux langages objets qui proposent les concepts d'héritage : C++, C#, Java, AS3, VB.NET et consorts. On écarte ici toutes les possibilités de générer des fichiers pur texte. Il est donc impossible de gérer autant de cas que dans l'approche par balises.

Cette approche consiste à abstraire l'implémentation concrète de la classe qui sera codée manuellement de son interface qui, elle, sera générée. Cette approche suit un des principe de base de la programmation orientée objet : le polymorphisme. Pour garder un code portable et réutilisable, il est préférable de faire référence à une classe par son interface, plutôt que par son implémentation, comme rappelé dans le livre Design Patterns :
  • Program to an interface, not an implementation.
  • Don't declare variables to be instances of particular concrete classes. Instead, commit only to an interface defined by an abstract class.
 

Le code généré est donc celui de l'interface (ou classe abstraite). Le code manuel est laissé dans l'implémentation. Lors d'une regénération, l'interface est écrasée, le code manuel n'est pas regénéré.
Cette approche a son propre Design Patterns, appelé "Generation Gap" qui est expliqué beaucoup plus en détail sur la page de John Vlissides. Il est intéressant de se rappeler également du Design Pattern "Patron de conception" lors de la mise en place de cette approche.

Si l'on souhaite pouvoir ajouter manuellement du comportement à la classe, il faudra prévoir d'exposer ce comportement dans les interfaces. Dans ce cas, il faut aller une étape plus loin et proposer :
  • Une interface générique entièrement générée;
  • Une interface pour le code manuel;
  • Une classe abstraite d'implémentation;
  • Une classe d'implémentation pour le code manuel.
Ce schéma est grandement inspiré de celui proposé dans la documentation de mod4j.

Utilisé par : Le Data Centric Development de FlashBuilder 4  pour générer le code AS3 issus de l'exposition des services (code généré depuis un WSDL par exemple).
C'est également ce principe qui est utilisé dans mod4j pour la génération des fichiers Java sur une grande partie des couches logicielles. 

Limitations
  • Le nombre de fichiers générés pour un seul concept est important. Il double le nombre de fichiers juste pour des raisons de génération.
  • Cette solution est intéressante mais ne peut pas fonctionner seule pour générer intégralement un projet d'entreprise JavaEE ou .NET : Une application d'entreprise nécessite plusieurs langages : Java, XML, JSP, HTML, Javascript, CSS, etc. Le problème est que tous ces fichiers générés ne sont pas tous des langages Orientés Objets. Il faudra donc la coupler avec une autre approche.
Note : pour .NET, il est préférable d'utiliser l'approche "classes partielles" expliquée plus bas (voir 2.3.1).
Re-note : en Java, il peut être intéressant d'utiliser AspectJ (voir 2.3.2).

2.2. L'approche « inclusion »
Cette approche peut être utilisée pour les fichiers qui proposent des concepts d'inclusion.
Elle peut être utilisée par Spring : un fichier de contexte Spring global inclut un fichier intégralement généré, puis un fichier qui contient le code manuel.
On peut supposer une approche équivalente pour le Javascript : le fichier HTML inclus un fichier Javascript entièrement généré et un autre qui contient le code manuel.

Utilisé par : mod4j propose une génération équivalente à l'approche abstraction pour les fichiers Spring.

Limitations :
  • Génération limitée aux représentations textuelles qui proposent des inclusions : Spring, Javascript (autres ?) .


2.3. Approches spécifiques aux langages

2.3.1. .NET et les "classes partielles"
Cette approche est spécifique à .NET et donc aux langages C# et VB.NET à partir de la version 2.0.
La possibilité de découper des classes en plusieurs morceaux et de les ré-assembler lors de la compilation rejoint l'approche "inclusion" mais spécifiquement à la plateforme .NET.
Cette fonctionnalité à été originalement mise en place dans le langage pour faciliter le découpage MVC, mais on peut très bien la mettre en place pour l'ensemble des classes .NET. Chaque classe peut donc être découpée en deux fichiers :
  • Un fichier "Personne.cs" qui contient le code manuel et qui ne sera pas impacté par une regénération ;
  • Un fichier "Personne-generated.cs" qui contient le code généré et qui sera écrasé lors de la prochaine génération.
Utilisé par : l'éditeur graphique de Visual Studio 2005 découpe les classes de cette façon.
Cette approche est également utilisée par SoftFluent Entities Builder pour la génération des applications .NET sous Visual Studio 2008.

Limitations :
  • Cette approche double le nombre de fichiers, mais contrairement à l'approche "abstraction", la génération par classe partielle .NET présente l’intérêt de ne pas doubler le nombre de concepts objets (pas besoin d'avoir des classes abstraites, etc.). D'un point de vue conception, on reste donc propre et c'est un bon point. Dommage que Java, en natif, ne dispose pas d'un mécanisme équivalent.
  • On ne peut pas générer l'intégralité d'un projet d'entreprise .NET avec cette solution. Comme pour la solution abstraction, il faudra prévoir une alternative pour les autres ressources du projet.


2.3.2. AspectJ et l'Inter Type Declaration
Dans un contexte de développement AOP, il est possible d'utiliser l'Inter Type Definition pour découper le code manuel et le code généré. Ici, on reste très axé sur du Java (ou du moins sur des langages Java au sens plus large). Le code généré est placé dans un fichier AspectJ (fichier .aj), le code manuel dans le fichier Java (fichier .java).
Lors de la compilation, le code du fichier AspectJ sera injecté dans le code Java. Lors d'une regénération, seul le fichier AspectJ est écrasé, le fichier Java reste intact.



Utilisé par : Spring Roo sur la plupart des fichiers Java.


A noter : Jboss Forge (qui ressemble étrangement à Spring Roo) ne fait que de la génération One-Time et ne propose donc pas de système équivalent aux fichiers AspectJ proposé par Spring Roo. Même chose pour le framework Play !
pour aller plus loin : ce comparatif explique la différence entre les 3 principaux RAD en Java : Spring Roo, Jboss Forge et Play !


Limitations :
  • Cette approche double le nombre de fichiers, mais contrairement à l'approche "abstraction", la génération par fichier AspectJ présente l’intérêt de ne pas doubler le nombre de concepts objets (pas besoin d'avoir des classes abstraites, etc.). Le fait que le fichier AspectJ soit intégré à la compilation offre également les facilités d'éditions dans l'IDE (complétion) ainsi que la validation statique des types.
  • On reste très (très) dépendant de la librairie AspectJ avec cette approche. La compilation n'est pas standard non plus. Par contre, pour l’exécution, on est indépendant d'AspectJ ( Roo is not Runtime).
  • On ne peut pas générer l'intégralité d'un projet JavaEE avec cette solution. Comme pour la solution abstraction, il faudra prévoir une alternative pour les autres ressources du projet.

3. Pour aller (encore) plus loin
  • Un comparatif des outils de génération de code (pas complet mais qui a le mérite d'exister) : language workbenches.



samedi 21 janvier 2012

Concepts de base des générateurs MDA


Lors de la création d'un générateur de code, il est intéressant de se poser les bonnes questions pour savoir si le code que l'on souhaite générer va pouvoir l'être intégralement.
Il existe 3 possibilités :
  1. Le concept du modèle fourni suffisamment d'informations pour pouvoir générer le code intégralement. Le code ne nécessite aucune reprise par le développeur. Lors de la modification du modèle, le code sera regénéré et écrase l'intégralité du code existant.
  2. Le concept du modèle fourni des informations pour la génération mais qui ne sont pas complète. Le code est généré en partie et le développeur devra reprendre le code généré pour l'enrichir à du code manuel. Lors de la modification du modèle, seul le code généré est écrasé. Le code manuel, quand à lui, est protégé et reste intact.
  3. Le concept du modèle fournit peu ou pas d'information pour générer le contenu. Le générateur va créer un squelette du fichier que le développeur devra codé manuellement. Lors de la modification du modèle, la regénération n'écrase pas le code.
Le cas 1 est le cas idéal du MDA : je modélise un concept et je génère 100% de la sémantique dans le code.
Le cas 3 est le pire cas. Il peut signifier que le DSL n'offre pas suffisamment de concept pour la génération. Il peut intervenir également lorsque l'on souhaite générer des fichiers trop techniques et dont les informations sont naturellement absente du modèle (qui reste fonctionnel).
Lors d'une regénération du code (suite à une modification du modèle), ces 2 cas ne sont pas problématiques : pour le cas 1, j'écrase l'intégralité du code; pour le cas 2, si le code existe déjà, je ne fais rien !
Le plus problématique, lors d'une regénération est le cas 2 : il faut garder intact les portions de code manuelles et écraser les portions de codes générées.

Pour info, ces cas sont répertoriés principalement sur des générateurs MDA. Il existe d'autres types de générateurs de code qui sont des moteurs de template et qui écrasent toujours le code existant en cas de modification du modèle (voir par exemple JET ou StringTemplate).

Références
le schéma est grandement inspiré de celui de Genuitec.