Génération de code à l'aide de T4

Je n'aime pas la génération de code et généralement, je le vois comme une "odeur". Si vous utilisez une génération de code, il y a de fortes chances que votre conception ou solution présente un problème! Alors peut-être qu'au lieu d'écrire un script pour générer des milliers de lignes de code, vous devriez prendre du recul, repenser votre problème et trouver une meilleure solution. Cela dit, il existe des situations où la génération de code pourrait être une bonne solution.

Dans cet article, je parlerai des avantages et des inconvénients de la génération de code, puis vous montrerai comment utiliser les modèles T4, l'outil de génération de code intégré à Visual Studio, à l'aide d'un exemple..

La génération de code est une mauvaise idée

J'écris un article sur un concept que je considère comme une mauvaise idée. Le plus souvent, il ne serait pas professionnel de ma part de vous donner un outil sans vous avertir de ses dangers..

La vérité est que la génération de code est très excitante: vous écrivez quelques lignes de code et vous obtenez beaucoup plus en retour que vous auriez peut-être à écrire manuellement. Il est donc facile de tomber dans un piège unique:

"Si le seul outil dont vous disposez est un marteau, vous avez tendance à voir chaque problème comme un clou" ". A. Maslow

Mais la génération de code est presque toujours une mauvaise idée. Je vous renvoie à ce message, qui explique la plupart des problèmes que je vois avec la génération de code. En bref, la génération de code se traduit par un code inflexible et difficile à gérer.

Voici quelques exemples de lieux où vous devriez ne pas Utiliser la génération de code:

  • Avec architecture distribuée générée par code vous exécutez un script qui génère les contrats de service et les implémentations et transforme par magie votre application en une architecture distribuée. Cela ne permet évidemment pas de reconnaître la causalité excessive des appels en cours qui ralentit considérablement sur le réseau et la nécessité de gérer correctement les exceptions et les transactions des systèmes distribués, etc..
  • Designers graphiques est ce que les développeurs Microsoft utilisent depuis des lustres (dans Windows / Web Forms et, dans une certaine mesure, dans des applications XAML) où ils glissent et déposent des widgets et des éléments d'interface utilisateur et voient le code (laid) de l'interface utilisateur généré pour eux en coulisse..
  • Objets nus est une approche du développement logiciel dans laquelle vous définissez votre modèle de domaine et le reste de votre application, y compris l'interface utilisateur et la base de données, tout est généré pour vous. Conceptuellement, c'est très proche de l'architecture pilotée par les modèles.
  • Architecture pilotée par les modèles est une approche du développement logiciel dans laquelle vous spécifiez votre domaine en détail à l’aide d’un PIM (Platform Independence Model). À l'aide de la génération de code, PIM est ensuite transformé en un modèle spécifique à la plate-forme (PSM), qu'un ordinateur peut exécuter. L'un des principaux arguments de vente de MDA est que vous spécifiez le PIM une seule fois et pouvez générer des applications Web ou bureautiques dans divers langages de programmation en appuyant simplement sur un bouton pouvant générer le code PSM souhaité..
    De nombreux outils RAD (Rapid Application Development) sont créés sur la base de cette idée: vous dessinez un modèle et cliquez sur un bouton pour obtenir une application complète. Certains de ces outils vont jusqu'à essayer de supprimer complètement les développeurs de l'équation dans laquelle on pense que les utilisateurs non techniques sont capables de modifier en toute sécurité le logiciel sans qu'il soit nécessaire de recourir aux développeurs..

J'allais également faire figurer le mappage relationnel-objet dans la liste, car certains ORM s'appuient fortement sur la génération de code pour créer le modèle de persistance à partir d'un modèle de données conceptuel ou physique. J'ai utilisé certains de ces outils et j'ai beaucoup souffert pour personnaliser le code généré. Cela dit, beaucoup de développeurs semblent vraiment les aimer, alors je l’ai laissé de côté (ou l’ai-je fait?!);)

Bien que certains de ces "outils" résolvent certains problèmes de programmation et réduisent les efforts et le coût de développement de logiciels requis, il existe un coût de maintenance caché énorme lié à l'utilisation de la génération de code, qui finira tôt ou tard par vous nuire le code généré que vous avez, plus cela va faire mal.

Je sais que beaucoup de développeurs sont de grands fans de la génération de code et écrivent chaque jour un nouveau script de génération de code. Si vous êtes dans ce camp et pensez que c'est un excellent outil pour beaucoup de problèmes, je ne vais pas me disputer avec vous. Après tout, cet article ne vise pas à prouver que la génération de code est une mauvaise idée.

Parfois, seulement parfois, la génération de code peut être une bonne idée

Très rarement cependant, je me trouve dans une situation où la génération de code convient bien au problème à résoudre et où les solutions alternatives seraient plus difficiles ou plus laides..

Voici quelques exemples de situations dans lesquelles la génération de code pourrait bien convenir:

  • Vous devez écrire beaucoup de code standard qui suit un modèle statique similaire. Dans ce cas, avant d'essayer de générer du code, réfléchissez bien au problème et essayez d'écrire correctement ce code (par exemple, utilisez des modèles orientés objet si vous écrivez du code OO). Si vous avez fait de gros efforts et que vous n'avez pas trouvé de bonne solution, la génération de code peut constituer un bon choix..
  • Vous utilisez très souvent des métadonnées statiques à partir d'une ressource et la récupération des données nécessite l'utilisation de chaînes magiques (et peut-être une opération coûteuse). Voici quelques exemples:
    • Métadonnées de code récupérées par réflexion: appeler du code en utilisant la réflexion nécessite des chaînes magiques; mais au moment de la conception, vous savez ce dont vous avez besoin. Vous pouvez utiliser la génération de code pour générer les artefacts requis. De cette façon, vous éviterez l'utilisation de réflexions au moment de l'exécution et / ou de chaînes magiques dans votre code. T4MVC est un excellent exemple de ce concept. Il crée des aides fortement typées qui éliminent l’utilisation de chaînes littérales dans de nombreux endroits..
    • Services Web de recherche statique: de temps en temps, je rencontre des services Web qui ne fournissent que des données statiques qui peuvent être récupérées en fournissant une clé, ce qui se termine par une chaîne magique dans la base de code. Dans ce cas, si vous pouvez récupérer par programme toutes les clés, vous pouvez générer une classe statique contenant toutes les clés et accéder aux valeurs de chaîne en tant que citoyens de première classe fortement typés dans votre base de code au lieu d'utiliser des chaînes magiques. Vous pouvez évidemment créer la classe manuellement; mais vous devrez également le maintenir, manuellement, chaque fois que les données changent. Vous pouvez ensuite utiliser cette classe pour frapper le service Web et mettre en cache le résultat afin que les appels suivants soient résolus à partir de la mémoire..
      Sinon, si cela est autorisé, vous pouvez simplement générer le service entier en code afin que le service de recherche ne soit pas requis au moment de l'exécution. Les deux solutions ont des avantages et des inconvénients, choisissez donc celle qui correspond à vos besoins. Ce dernier n’est utile que si les clés ne sont utilisées que par l’application et ne sont pas fournies par l’utilisateur; sinon, tôt ou tard, les données de service auront été mises à jour sans que le code soit généré et la recherche lancée par l'utilisateur échouera.
    • Tables de consultation statiques: Cela ressemble beaucoup aux services Web statiques, mais les données résident dans un magasin de données, par opposition à un service Web.

Comme mentionné ci-dessus, la génération de code rend le code inflexible et difficile à maintenir; Par conséquent, si la nature du problème que vous résolvez est statique et ne nécessite pas de maintenance fréquente, la génération de code peut être une bonne solution.!

Ce n’est pas parce que votre problème s’inscrit dans l’une des catégories ci-dessus que la génération de code convient parfaitement. Vous devriez quand même essayer d'évaluer des solutions alternatives et peser vos options.

En outre, si vous optez pour la génération de code, veillez à toujours écrire des tests unitaires. Pour une raison quelconque, certains développeurs pensent que le code généré ne nécessite pas de tests unitaires. Peut-être pensent-ils que cela est généré par des ordinateurs et que les ordinateurs ne font pas d'erreur! Je pense que le code généré nécessite tout autant (sinon plus) une vérification automatisée. Personnellement, j'ai créé ma code: j'écris d'abord les tests, je les lance pour les voir échouer, puis je génère le code et je vois les tests réussis.

Boîte à outils de transformation de modèle de texte

Il existe un moteur de génération de code génial dans Visual Studio appelé Kit de transformation de modèles de texte (AKA, T4)..

De MSDN:

Les modèles de texte sont composés des parties suivantes:

  • Les directives: éléments qui contrôlent le traitement du modèle.
  • Blocs de texte: contenu copié directement dans la sortie.
  • Blocs de contrôle: code de programme qui insère des valeurs variables dans le texte et contrôle des parties conditionnelles ou répétées du texte.

Au lieu de parler du fonctionnement de T4, je voudrais utiliser un exemple concret. Donc, voici un problème que j'ai rencontré il y a un moment pour lequel j'ai utilisé T4. J'ai une bibliothèque open source .NET appelée Humanizer. L’une des choses que je souhaitais fournir dans Humanizer était une API facile à utiliser pour les développeurs pour travailler avec Date et heure.

J'ai envisagé quelques variantes de l'API et à la fin, je me suis décidé pour cela:

In.January // Renvoie le 1er janvier de l'année en cours In.FebruaryOf (2009) // Renvoie le 1er février 2009 Le.Janvier.Le 4ème // Renvoie le 4 janvier de l'année en cours On.Février.Le (12) // Renvoie le 12 février de l'année en cours In.One.Second // DateTime.UtcNow.AddSeconds (1); In.Two.Minutes // Avec la méthode From correspondante In.Three.Hours // Avec la méthode From correspondante In.Five.Days // Avec la méthode From correspondante In.Six.Weeks // Avec la méthode From correspondante In.Seven.Months / / Avec la méthode From correspondante In.Eight.years // Avec la méthode From correspondante In.Two.SecondsFrom (DateTime dateTime)

Une fois que je savais à quoi allait ressembler mon API, j’ai réfléchi à différentes façons de résoudre ce problème et proposé quelques solutions orientées objet, mais toutes nécessitaient un peu de code passe-partout et celles qui ne le faisaient pas ne le feraient pas. donnez-moi l'API publique propre que je voulais. J'ai donc décidé d'aller avec la génération de code.

Pour chaque variante, j'ai créé un fichier T4 distinct:

  • In.Months.tt pour En janvier et In.FebrurayOf () etc.
  • On.Days.tt pour On.January.The4th, Le.février.le (12) etc.
  • In.SomeTimeFrom.tt pour En une seconde, In.TwoSecondsFrom (), Trois minutes etc.

Ici je vais discuter Un jour. Le code est copié ici pour votre référence:

<#@ template debug="true" hostSpecific="true" #> <#@ output extension=".cs" #> <#@ Assembly Name="System.Core" #> <#@ Assembly Name="System.Windows.Forms" #> <#@ assembly name="$(SolutionDir)Humanizer\bin\Debug\Humanizer.dll" #> <#@ import namespace="System" #> <#@ import namespace="Humanizer" #> <#@ import namespace="System.IO" #> <#@ import namespace="System.Diagnostics" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Collections" #> <#@ import namespace="System.Collections.Generic" #> en utilisant le système; espace de noms Humanizer classe partielle publique On  <# const int leapYear = 2012; for (int month = 1; month <= 12; month++)  var firstDayOfMonth = new DateTime(leapYear, month, 1); var monthName = firstDayOfMonth.ToString("MMMM");#> ///  /// Fournit des accesseurs de date courants pour <#= monthName #> ///  classe publique <#= monthName #> ///  /// Le nième jour de <#= monthName #> de l'année en cours ///  static DateTime public The (int dayNumber) renvoie le nouveau DateTime (DateTime.Now.Year, <#= month #>, dayNumber);  <#for (int day = 1; day <= DateTime.DaysInMonth(leapYear, month); day++)  var ordinalDay = day.Ordinalize();#> ///  /// Le <#= ordinalDay #> jour de <#= monthName #> de l'année en cours ///  statique DateTime public le<#= ordinalDay #> get retour new DateTime (DateTime.Now.Year, <#= month #>, <#= day #>)  <##>  <##> 

Si vous extrayez ce code dans Visual Studio ou souhaitez utiliser T4, assurez-vous d'avoir installé Tangible T4 Editor pour Visual Studio. Il fournit IntelliSense, la surbrillance de la syntaxe T4, le débogueur T4 avancé et la transformation T4 lors de la génération..

Le code peut sembler un peu effrayant au début, mais c'est juste un script très similaire au langage ASP. Lors de la sauvegarde, cela générera une classe appelée Sur avec 12 sous-classes, une par mois (par exemple, janvier, février etc), chacune avec des propriétés statiques publiques qui renvoient un jour spécifique de ce mois. Brisons le code et voyons comment cela fonctionne.

Les directives

La syntaxe des directives est la suivante: <#@ DirectiveName [AttributeName = "AttributeValue"]… #>. Vous pouvez en savoir plus sur les directives ici.

J'ai utilisé les directives suivantes dans le code:

Modèle

<#@ template debug="true" hostSpecific="true" #>

La directive Template possède plusieurs attributs qui vous permettent de spécifier différents aspects de la transformation..

Si la déboguer attribut est vrai, le fichier de code intermédiaire contiendra des informations permettant au débogueur d'identifier plus précisément la position dans votre modèle où une interruption ou une exception s'est produite. Je laisse toujours ça comme vrai.

Sortie

<#@ output extension=".cs" #>

La directive Output permet de définir l’extension du nom de fichier et le codage du fichier transformé. Ici nous mettons l'extension à .cs ce qui signifie que le fichier généré sera en C # et que le nom du fichier sera On.Days.cs.

Assemblée

<#@ assembly Name="System.Core" #>

Ici nous sommes en train de charger System.Core afin que nous puissions l'utiliser dans les blocs de code plus bas.

La directive Assembly charge un assembly afin que votre code de modèle puisse utiliser ses types. L'effet est similaire à l'ajout d'une référence d'assembly dans un projet Visual Studio..

Cela signifie que vous pouvez tirer pleinement parti du framework .NET dans votre modèle T4. Par exemple, vous pouvez utiliser ADO.NET pour accéder à une base de données, lire des données d’une table et les utiliser pour la génération de code..

Plus bas, j'ai la ligne suivante:

<#@ assembly name="$(SolutionDir)Humanizer\bin\Debug\Humanizer.dll" #>

C'est un peu intéressant. dans le On.Days.tt template J'utilise la méthode Ordinalize de Humanizer qui convertit un nombre en chaîne ordinale, utilisée pour désigner la position dans une séquence ordonnée telle que 1st, 2nd, 3rd, 4th. Ceci est utilisé pour générer Le 1er, Le 2ème etc.

De l'article MSDN:

Le nom de l'assembly doit être l'un des suivants:

  • Le nom fort d’une assemblée du GAC, tel que System.Xml.dll. Vous pouvez également utiliser la forme longue, telle que name = "System.Xml, Version = 4.0.0.0, Culture = neutre, PublicKeyToken = b77a5c561934e089". Pour plus d'informations, voir AssemblyName.
  • Le chemin absolu de l'assemblée.

System.Core vit à GAC, nous pourrions donc facilement utiliser son nom; mais pour Humanizer, nous devons fournir le chemin absolu. Évidemment, je ne veux pas coder en dur mon chemin local, alors j’ai utilisé $ (SolutionDir) qui est remplacé par le chemin d'accès de la solution lors de la génération du code. De cette façon, la génération de code fonctionne bien pour tout le monde, peu importe où ils conservent le code..

Importation

<#@ import namespace="System" #>

La directive import vous permet de faire référence à des éléments d'un autre espace de noms sans fournir de nom complet. C'est l'équivalent du en utilisant déclaration en C # ou importations en Visual Basic.

En haut, nous définissons tous les espaces de noms nécessaires dans les blocs de code. le importation Les blocs que vous voyez sont principalement insérés par T4 Tangible. La seule chose que j'ai ajoutée était:

<#@ import namespace="Humanizer" #> 

Donc je peux écrire plus tard:

var ordinalDay = day.Ordinalize (); 

Sans le importation déclaration et spécifiant la Assemblée par chemin, au lieu d’un fichier C #, j’aurais eu une erreur de compilation en me plaignant de ne pas trouver le Ordinaliser méthode sur entier.

Blocs de texte

Un bloc de texte insère du texte directement dans le fichier de sortie. En haut, j'ai écrit quelques lignes de code C # qui sont directement copiées dans le fichier généré:

en utilisant le système; espace de noms Humanizer classe partielle publique On 

Plus bas, entre les blocs de contrôle, j'ai quelques autres blocs de texte pour la documentation de l'API, les méthodes et aussi pour la fermeture des crochets.

Blocs de contrôle

Les blocs de contrôle sont des sections de code de programme utilisées pour transformer les modèles. La langue par défaut est C #.

Remarque: La langue dans laquelle vous écrivez le code dans les blocs de contrôle n'est pas liée à la langue du texte généré.

Il existe trois types différents de blocs de contrôle: Standard, Expression et Class Feature.. 

De MSDN:

  • <# Standard control blocks #> peut contenir des déclarations.
  • <#= Expression control blocks #> peut contenir des expressions.
  • <#+ Class feature control blocks #> peut contenir des méthodes, des champs et des propriétés.

Jetons un coup d'oeil aux blocs de contrôles que nous avons dans le modèle d'exemple:

<# const int leapYear = 2012; for (int month = 1; month <= 12; month++)  var firstDayOfMonth = new DateTime(leapYear, month, 1); var monthName = firstDayOfMonth.ToString("MMMM");#> ///  /// Fournit des accesseurs de date courants pour <#= monthName #> ///  classe publique <#= monthName #> ///  /// Le nième jour de <#= monthName #> de l'année en cours ///  static DateTime public The (int dayNumber) renvoie le nouveau DateTime (DateTime.Now.Year, <#= month #>, dayNumber);  <#for (int day = 1; day <= DateTime.DaysInMonth(leapYear, month); day++)  var ordinalDay = day.Ordinalize();#> ///  /// Le <#= ordinalDay #> jour de <#= monthName #> de l'année en cours ///  statique DateTime public le<#= ordinalDay #> get retour new DateTime (DateTime.Now.Year, <#= month #>, <#= day #>)  <##>  <##>

Pour moi personnellement, la chose la plus déroutante à propos de T4 est les blocs de contrôle d'ouverture et de fermeture, car ils se mélangent un peu avec les crochets du bloc de texte (si vous générez du code pour un langage d'accolade comme C #). Je trouve que le moyen le plus simple de régler ce problème est de fermer (#>) le bloc de contrôle dès mon ouverture (<#) puis écrivez le code à l'intérieur.

En haut, à l'intérieur du bloc de contrôle standard, je définis année bissextile comme une valeur constante. C'est pour que je puisse générer une entrée pour le 29 février. Ensuite, j'itère sur 12 mois pour chaque mois obtenir le premier jour du mois et le monthName. Je ferme ensuite le bloc de contrôle pour écrire un bloc de texte pour la classe de mois et sa documentation XML. le monthName est utilisé comme nom de classe et dans les commentaires XML (à l'aide de blocs de contrôle d'expression). Le reste n'est que du code C # normal avec lequel je ne vais pas vous ennuyer.

Conclusion

Dans cet article, j'ai parlé de la génération de code, fourni quelques exemples de situations dans lesquelles la génération de code pouvait être dangereuse ou utile, et également expliqué comment utiliser des modèles T4 pour générer du code à partir de Visual Studio à l'aide d'un exemple réel..

Si vous souhaitez en savoir plus sur le T4, vous pouvez trouver beaucoup de contenu sur le blog de Oleg Sych..