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.