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.