Codegeneratie met T4

Ik houd niet van codegeneratie en meestal zie ik het als een "geur". Als u gebruikmaakt van het genereren van codes, is er een goede kans dat er iets mis is met uw ontwerp of oplossing! Dus misschien in plaats van een script te schrijven om duizenden regels code te genereren, moet je een stapje terug doen, opnieuw over je probleem nadenken en een betere oplossing bedenken. Met dat gezegd, er zijn situaties waarin het genereren van code een goede oplossing kan zijn.

In dit bericht zal ik het hebben over voor- en nadelen van het genereren van code en vervolgens laten zien hoe je T4-sjablonen, het ingebouwde hulpmiddel voor het genereren van code in Visual Studio, kunt gebruiken met een voorbeeld.

Codegeneratie is een slecht idee

Ik schrijf een bericht over een concept waarvan ik denk dat het een slecht idee is, vaker wel dan niet en het zou onprofessioneel van me zijn als ik je een hulpmiddel overhandigde en je niet waarschuwde voor de gevaren ervan.

De waarheid is dat het genereren van code best spannend is: je schrijft een paar regels code en je krijgt veel meer in ruil daarvoor zou je misschien handmatig moeten schrijven. Dus het is gemakkelijk om er een one-size-fits-all val mee te maken:

"Als het enige hulpmiddel dat je hebt een hamer is, heb je de neiging om elk probleem als een spijker te zien" ". A. Maslow

Maar het genereren van code is bijna altijd een slecht idee. Ik verwijs u naar dit bericht, dat de meeste problemen die ik zie met het genereren van codes verklaart. Kort samengevat resulteert het genereren van code in inflexibele en moeilijk te onderhouden code.

Hier zijn enkele voorbeelden van waar u zou moeten zijn niet gebruik code generatie:

  • Met code gegenereerde gedistribueerde architectuur u voert een script uit dat de servicecontracten en de implementaties genereert en op magische wijze uw toepassing omzet in een gedistribueerde architectuur. Dat is duidelijk niet de erkenning van de buitensporige praatjes van in-process-oproepen die drastisch vertragen over het netwerk en de behoefte aan juiste uitzondering en transactieverwerking van gedistribueerde systemen enzovoort.
  • Visual GUI-ontwerpers is wat Microsoft-ontwikkelaars al jaren gebruiken (in Windows / Web Forms en tot op zekere hoogte op XAML gebaseerde applicaties) waar ze widgets en UI-elementen slepen en neerzetten en de (lelijke) UI-code zien die voor hen achter de schermen is gegenereerd.
  • Naakte voorwerpen is een benadering van softwareontwikkeling waarbij u uw domeinmodel definieert en de rest van uw toepassing, inclusief de gebruikersinterface en de database, voor u wordt gegenereerd. Conceptueel komt het dicht in de buurt van Model Driven Architecture.
  • Model driven architectuur is een benadering voor softwareontwikkeling waarbij u uw domein in detail specificeert met behulp van een Platform Independence Model (PIM). Door gebruik te maken van codegeneratie wordt PIM later omgezet in een Platform Specific Model (PSM), dat een computer kan uitvoeren. Een van de belangrijkste verkoopargumenten van MDA is dat u de PIM eenmaal opgeeft en web- of bureaubladtoepassingen in verschillende programmeertalen kunt genereren door op een knop te drukken die de gewenste PSM-code kan genereren.
    Op basis van dit idee worden veel RAD-tools (Rapid Application Development) gemaakt: u tekent een model en klikt op een knop om een ​​volledige toepassing te krijgen. Sommige van deze tools gaan zelfs zover dat ze proberen om ontwikkelaars volledig uit de vergelijking te verwijderen, waarbij van niet-technische gebruikers wordt verondersteld dat ze veilige wijzigingen in de software kunnen aanbrengen zonder dat er een ontwikkelaar nodig is.

Ik zou Object Relational Mapping ook in de lijst plaatsen, omdat sommige ORM's sterk afhankelijk zijn van het genereren van code om het persistentiemodel te maken van een conceptueel of fysiek gegevensmodel. Ik heb een aantal van deze hulpmiddelen gebruikt en een flinke dosis pijn gedaan om de gegenereerde code aan te passen. Dat gezegd hebbende, veel ontwikkelaars lijken ze echt leuk te vinden, dus ik heb dat gewoon weggelaten (of niet ?!);)

Hoewel sommige van deze 'hulpprogramma's' sommige van de programmeerproblemen oplossen en de vereiste inspanningen en kosten van softwareontwikkeling verminderen, zijn er enorme onderhoudskosten in het gebruik van codegeneratie die u vroeg of laat zullen bijten en hoe meer gegenereerde code die u heeft, des te meer pijn het gaat doen.

Ik weet dat veel ontwikkelaars grote fans zijn van het genereren van code en elke dag een nieuw codegeneratiescript schrijven. Als je in dat kamp bent en denkt dat het een geweldig hulpmiddel is voor veel problemen, ga ik niet met je ruzie maken. Het gaat er tenslotte niet om dat bewijzen dat het genereren van code een slecht idee is.

Soms, slechts soms, zou codegeneratie een goed idee kunnen zijn

Maar heel zelden kom ik in een situatie terecht waarin codegeneratie geschikt is voor het probleem en de alternatieve oplossingen moeilijker of lelijker zijn.

Hier zijn een paar voorbeelden van waar codegeneratie een goede match kan zijn:

  • Je moet veel boilerplate code schrijven die een vergelijkbaar statisch patroon volgt. Voordat u de codegeneratie probeert, moet u in dit geval heel goed nadenken over het probleem en proberen deze code correct te schrijven (bijvoorbeeld objectgeoriënteerde patronen gebruiken als u OO-code schrijft). Als u hard hebt geprobeerd en geen goede oplossing hebt gevonden, is codegeneratie misschien een goede keuze.
  • Je gebruikt heel vaak een aantal statische metagegevens van een bron en het ophalen van de gegevens vereist het gebruik van magische tekenreeksen (en misschien is het een kostbare bewerking). Hier zijn een paar voorbeelden:
    • Codemetadata opgehaald door reflectie: het aanroepen van code met reflectie vereist magische reeksen; maar op het moment van ontwerp weet je wat je nodig hebt. Je kunt code genereren om de benodigde artefacten te genereren. Op deze manier voorkomt u het gebruik van reflecties tijdens runtime en / of magic strings in uw code. Een goed voorbeeld van dit concept is T4MVC dat sterk getypte helpers maakt die het gebruik van letterlijke tekenreeksen op veel plaatsen elimineren.
    • Statische opzoekwebservices: zo nu en dan kom ik webservices tegen die alleen statische gegevens leveren die kunnen worden opgehaald door een sleutel aan te bieden, die als een magische tekenreeks in de codebase terechtkomt. Als u in dit geval alle sleutels programmatisch kunt ophalen, kunt u met code een statische klasse genereren die alle sleutels bevat en toegang krijgen tot de tekenreekswaarden als sterk getypeerde eersteklasburgers in uw codebase in plaats van magische reeksen te gebruiken. Je zou de klasse natuurlijk handmatig kunnen maken; maar je zou het ook manueel moeten onderhouden, elke keer dat de gegevens veranderen. U kunt deze klasse vervolgens gebruiken om de webservice te activeren en het resultaat in de cache opslaan, zodat de volgende oproepen vanuit het geheugen worden opgelost.
      U kunt ook, indien toegestaan, de gehele service in code genereren, zodat de opzoekservice niet vereist is tijdens runtime. Beide oplossingen hebben een aantal voor- en nadelen, dus kies degene die bij u past. Dit laatste is alleen nuttig als de sleutels alleen door de toepassing worden gebruikt en niet door de gebruiker worden verstrekt; anders zal er vroeg of laat een moment zijn dat de servicegegevens zijn bijgewerkt maar dat u de code niet hebt gegenereerd en dat de door de gebruiker gestarte lookup mislukt.
    • Statische opzoektabellen: Dit komt sterk overeen met statische webservices, maar de gegevens leven in een gegevensopslag in tegenstelling tot een webservice.

Zoals hierboven vermeld, zorgt het genereren van code voor inflexibele en moeilijk te onderhouden code; dus als de aard van het probleem dat u oplost statisch is en geen frequent onderhoud vereist, dan is het genereren van code een goede oplossing!

Alleen omdat uw probleem in een van de bovenstaande categorieën past, betekent dit niet dat het genereren van codes hiervoor geschikt is. U moet nog steeds proberen alternatieve oplossingen te evalueren en uw opties af te wegen.

En als je gaat voor het genereren van code, zorg er dan voor dat je nog steeds unit tests schrijft. Om een ​​of andere reden denken sommige ontwikkelaars dat de gegenereerde code geen unit testing vereist. Misschien denken ze dat het wordt gegenereerd door computers en computers geen fouten maken! Ik denk dat de gegenereerde code evenveel (zo niet meer) geautomatiseerde verificatie vereist. Ik persoonlijk TDD mijn codegeneratie: ik schrijf eerst de tests, voer ze uit om ze te zien falen, genereer dan de code en zie de tests slagen.

Text Template Transformation Toolkit

Er is een geweldige code generatie-engine in Visual Studio genaamd Text Template Transformation Toolkit (AKA, T4).

Van MSDN:

Tekstsjablonen bestaan ​​uit de volgende delen:

  • richtlijnen: elementen die bepalen hoe de sjabloon wordt verwerkt.
  • Tekstblokken: inhoud die direct wordt gekopieerd naar de uitvoer.
  • Besturingsblokken: programmacode die variabele waarden invoegt in de tekst en voorwaardelijke of herhaalde delen van de tekst beheert.

In plaats van te praten over hoe T4 werkt, zou ik een echt voorbeeld willen gebruiken. Dus hier is een probleem waar ik een tijdje geleden mee geconfronteerd ben waarvoor ik T4 gebruikte. Ik heb een open source .NET-bibliotheek genaamd Humanizer. Een van de dingen die ik in Humanizer wilde aanbieden, was een vloeiende ontwikkelaarsvriendelijke API om mee te werken Datum Tijd.

Ik heb een aantal varianten van de API overwogen en heb hier uiteindelijk voor gekozen:

In.januari // Geeft 1 januari van het lopende jaar In.FebruaryOf (2009) // Geeft als resultaat 1 februari van 2009 On.January.The4th // Geeft als resultaat 4 januari van het lopende jaar On.Februari.De (12) // Retourneert 12 februari van het lopende jaar In.One.Second // DateTime.UtcNow.AddSeconds (1); In.Twee minuten.Minuten // Met bijbehorende Van-methode In.Three.Hours // Met bijbehorende Van-methode In.Five.Days // Met bijbehorende Van-methode In.Six.Weeks // Met bijbehorende Van-methode In.Seven.Months / / Met bijbehorende Van-methode In.Eight.Years // Met bijbehorende Van-methode In.Two.SecondsFrom (DateTime dateTime)

Nadat ik wist hoe mijn API eruit zou zien, dacht ik aan een paar verschillende manieren om dit aan te pakken en een paar objectgeoriënteerde oplossingen aan te scherpen, maar ze hadden allemaal een behoorlijk beetje boilerplate code nodig en degenen die dat niet deden, wilden niet geef me de schone openbare API die ik wilde. Dus besloot ik om met codegeneratie te gaan.

Voor elke variatie heb ik een apart T4-bestand gemaakt:

  • In.Months.tt voor In januari en In.FebrurayOf () enzovoorts.
  • On.Days.tt voor On.January.The4th, On.February.The (12) enzovoorts.
  • In.SomeTimeFrom.tt voor In.One.Second, In.TwoSecondsFrom (), In.Three.Minutes enzovoorts.

Hier zal ik bespreken Op dagen. De code wordt hier voor uw referentie gekopieerd:

<#@ 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" #> systeem gebruiken; naamruimte Humanizer public partial class On  <# const int leapYear = 2012; for (int month = 1; month <= 12; month++)  var firstDayOfMonth = new DateTime(leapYear, month, 1); var monthName = firstDayOfMonth.ToString("MMMM");#> ///  /// Biedt vloeiende toegang tot datum voor <#= monthName #> ///  openbare les <#= monthName #> ///  /// De negende dag van <#= monthName #> van het lopende jaar ///  public static DateTime The (int dayNumber) retourneer nieuwe DateTime (DateTime.Now.Year, <#= month #>, dayNumber);  <#for (int day = 1; day <= DateTime.DaysInMonth(leapYear, month); day++)  var ordinalDay = day.Ordinalize();#> ///  /// De <#= ordinalDay #> dag van <#= monthName #> van het lopende jaar ///  public static DateTime The<#= ordinalDay #> krijg retourneer nieuwe DateTime (DateTime.Now.Year, <#= month #>, <#= day #>);  <##>  <##> 

Als je deze code uitcheckt in Visual Studio of wilt werken met T4, zorg er dan voor dat je de Tangible T4 Editor voor Visual Studio hebt geïnstalleerd. Het biedt IntelliSense, T4 Syntax-Highlighting, Advanced T4 Debugger en T4 Transform on Build.

De code lijkt misschien een beetje eng in het begin, maar het is gewoon een script dat erg op de ASP-taal lijkt. Na het opslaan, genereert dit een klasse genaamd Op met 12 subklassen, één per maand (bijvoorbeeld, januari-, februari enz.), elk met openbare statische eigenschappen die een specifieke dag in die maand retourneren. Laten we de code uit elkaar halen en zien hoe het werkt.

richtlijnen

De syntaxis van richtlijnen is als volgt: <#@ DirectiveName [AttributeName = "AttributeValue"]… #>. Je kunt hier meer over richtlijnen lezen.

Ik heb de volgende richtlijnen in de code gebruikt:

Sjabloon

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

De Template-instructie heeft verschillende attributen waarmee u verschillende aspecten van de transformatie kunt specificeren.

Als het debug kenmerk is waar, het tussencodebestand bevat informatie waarmee de foutopsporingsfunctie de positie in uw sjabloon nauwkeuriger kan identificeren waar een pauze of uitzondering heeft plaatsgevonden. Ik laat dit altijd zo waar.

uitgang

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

De Output-instructie wordt gebruikt om de extensie en de codering van het getransformeerde bestand te definiëren. Hier hebben we de extensie ingesteld op .cs wat betekent dat het gegenereerde bestand in C # staat en de bestandsnaam zal zijn On.Days.cs.

bijeenkomst

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

Hier laden we System.Core zodat we het verderop in de codeblokken kunnen gebruiken.

De Assembly-instructie laadt een assembly zodat uw sjablooncode de typen kan gebruiken. Het effect is vergelijkbaar met het toevoegen van een assemblyverwijzing in een Visual Studio-project.

Dit betekent dat u ten volle kunt profiteren van het .NET-framework in uw T4-sjabloon. U kunt bijvoorbeeld ADO.NET gebruiken om een ​​database te raken, sommige gegevens uit een tabel te lezen en die te gebruiken voor het genereren van codes.

Verderop heb ik de volgende regel:

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

Dit is een beetje interessant. In de On.Days.tt template Ik gebruik de Ordinalize-methode van Humanizer die een nummer omzet in een ordinale reeks, die wordt gebruikt om de positie aan te duiden in een geordende volgorde zoals 1e, 2e, 3e, 4e. Dit wordt gebruikt om te genereren De 1e, De 2e enzovoorts.

Uit het MSDN-artikel:

De naam van de assembly moet een van de volgende zijn:

  • De sterke naam van een assembly in de GAC, zoals system.xml.dll. U kunt ook de lange vorm gebruiken, zoals name = "System.Xml, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089". Zie voor meer informatie AssemblyName.
  • Het absolute pad van de vergadering.

System.Core leeft in GAC, dus we kunnen gewoon zijn naam gebruiken; maar voor Humanizer moeten we het absolute pad bieden. Natuurlijk wil ik mijn lokale pad niet hardcoderen, dus ik gebruikte het $ (SolutionDir) die wordt vervangen door het pad waarin de oplossing leeft tijdens het genereren van de code. Op deze manier werkt de codegeneratie prima voor iedereen, ongeacht waar ze de code bewaren.

Importeren

<#@ import namespace="System" #>

Met de importrichtlijn kunt u verwijzen naar elementen in een andere naamruimte zonder een volledig gekwalificeerde naam op te geven. Het is het equivalent van de gebruik makend van verklaring in C # of invoer in Visual Basic.

Op de top definiëren we alle namespaces die we nodig hebben in de codeblokken. De importeren blokken die je ziet, er zijn meestal ingevoegd door T4 Tangible. Het enige dat ik heb toegevoegd was:

<#@ import namespace="Humanizer" #> 

Dus ik kan later schrijven:

var ordinalDay = day.Ordinalize (); 

Zonder de importeren verklaring en specificatie van de bijeenkomst per pad, in plaats van een C # -bestand, zou ik een compileerfout hebben gekregen die klaagde over het niet vinden van de Ordinalize methode op integer.

Tekstblokken

Een tekstblok voegt tekst direct in het uitvoerbestand in. Bovenaan heb ik een paar regels C # -code geschreven die direct in het gegenereerde bestand worden gekopieerd:

systeem gebruiken; naamruimte Humanizer public partial class On 

Verderop, tussen controleblokken, heb ik een aantal andere tekstblokken voor API-documentatie, methoden en ook voor het sluiten van haakjes.

Besturingsblokken

Besturingsblokken zijn secties van programmacode die worden gebruikt om de sjablonen te transformeren. De standaardtaal is C #.

Notitie: De taal waarin u de code in de besturingsblokken schrijft, is niet gerelateerd aan de taal van de gegenereerde tekst.

Er zijn drie verschillende soorten besturingsblokken: standaard, expressie en klassenfunctie. 

Van MSDN:

  • <# Standard control blocks #> kan uitspraken bevatten.
  • <#= Expression control blocks #> kan uitdrukkingen bevatten.
  • <#+ Class feature control blocks #> kan methoden, velden en eigenschappen bevatten.

Laten we eens kijken naar de besturingsblokken die we in de voorbeeldsjabloon hebben:

<# const int leapYear = 2012; for (int month = 1; month <= 12; month++)  var firstDayOfMonth = new DateTime(leapYear, month, 1); var monthName = firstDayOfMonth.ToString("MMMM");#> ///  /// Biedt vloeiende toegang tot datum voor <#= monthName #> ///  openbare les <#= monthName #> ///  /// De negende dag van <#= monthName #> van het lopende jaar ///  public static DateTime The (int dayNumber) retourneer nieuwe DateTime (DateTime.Now.Year, <#= month #>, dayNumber);  <#for (int day = 1; day <= DateTime.DaysInMonth(leapYear, month); day++)  var ordinalDay = day.Ordinalize();#> ///  /// De <#= ordinalDay #> dag van <#= monthName #> van het lopende jaar ///  public static DateTime The<#= ordinalDay #> krijg retourneer nieuwe DateTime (DateTime.Now.Year, <#= month #>, <#= day #>);  <##>  <##>

Voor mij persoonlijk is het meest verwarrende aan T4 de openings- en sluitingsbesturingsblokken, omdat ze nogal gemengd worden met de haakjes in het tekstblok (als je code genereert voor een accolade-haakstaal zoals C #). Ik vind de gemakkelijkste manier om hiermee om te gaan, is om te sluiten (#>) het besturingsblok zodra ik open (<#) en schrijf de code vervolgens in.

Aan de bovenkant, binnen het standaardbesturingsblok, definieer ik schrikkeljaar als een constante waarde. Dit is zodat ik een bericht kan genereren voor 29 februari. Vervolgens herhaal ik meer dan 12 maanden voor elke maand om de firstDayOfMonth en de monthName. Ik sluit vervolgens het besturingsblok om een ​​tekstblok voor de maandklasse en de XML-documentatie te schrijven. De monthName wordt gebruikt als een klassenaam en in XML-opmerkingen (met behulp van blokken voor expressiecontrole). De rest is gewoon normale C # -code waar ik je niet mee vervul.

Conclusie

In deze post heb ik het gehad over het genereren van codes, een paar voorbeelden gegeven van wanneer het genereren van code gevaarlijk of nuttig kon zijn en ook liet zien hoe je T4-sjablonen kunt gebruiken om code te genereren van Visual Studio met een echt voorbeeld.

Als je meer wilt weten over T4, kun je veel geweldige content vinden op de blog van Oleg Sych.