Kodegenerering ved hjelp av T4

Jeg misliker kodegenerering og vanligvis ser jeg det som en "lukt". Hvis du bruker kodegenerering av noe slag, er det en god sjanse at noe er galt med design eller løsning! Så kanskje i stedet for å skrive et skript for å generere tusenvis av kodelinjer, bør du ta et skritt tilbake, tenke på problemet ditt igjen og komme opp med en bedre løsning. Med det sagt er det situasjoner der kodegenerering kan være en god løsning.

I dette innlegget vil jeg snakke om fordeler og ulemper med kodegenerering og deretter vise deg hvordan du bruker T4-maler, det innebygde kodegenereringsverktøyet i Visual Studio, ved hjelp av et eksempel.

Kodegenerering er en dårlig idé

Jeg skriver et innlegg om et konsept som jeg synes er en dårlig ide, oftere enn ikke, og det ville være uprofesjonelt av meg hvis jeg ga deg et verktøy og ikke advarte deg om farene sine.

Sannheten er at kodegenerering er ganske spennende: du skriver noen linjer med kode og du får mye mer av det i retur at du kanskje må skrive manuelt. Så det er lett å falle i en one-size-fits-all felle med det:

"Hvis det eneste verktøyet du har er en hammer, har du en tendens til å se alle problemer som en spiker" ". A. Maslow

Men kodegenerering er nesten alltid en dårlig ide. Jeg henviser deg til dette innlegget, som forklarer de fleste problemene jeg ser med kodegenerering. I et nøtteskall resulterer kodegenerering i ubøyelig og vanskelig å vedlikeholde kode.

Her er noen eksempler på hvor du skal ikke bruk kodegenerering:

  • Med kode generert distribuert arkitektur Du kjører et skript som genererer servicekontrakter og implementeringer, og gir deg en magisk omlegging til en distribuert arkitektur. Det klarer tydeligvis ikke å erkjenne den overdrevne chattinessen av prosesssamtaler som drar dramatisk over nettverket og behovet for riktig unntak og transaksjonshåndtering av distribuerte systemer og så videre.
  • Visual GUI designere er det som Microsoft-utviklere har brukt i årevis (i Windows / Web Forms og i noen grad XAML-baserte applikasjoner) der de drar og slipper widgets og UI-elementer og ser den (stygge) UI-koden generert for dem bak kulissene.
  • Naken objekter er en tilnærming til programvareutvikling der du definerer domenemodellen din og resten av søknaden din, inkludert brukergrensesnittet og databasen, blir alle generert for deg. Konseptuelt er det svært nær modelldrevet arkitektur.
  • Modelldrevet arkitektur er en tilnærming til programvareutvikling hvor du spesifiserer domenet ditt i detaljer ved hjelp av en plattform uavhengighetsmodell (PIM). Ved hjelp av kodegenerering, blir PIM senere omgjort til en plattformspesifikk modell (PSM) som en datamaskin kan kjøre. En av de viktigste salgsargumentene til MDA, er at du spesifiserer PIM en gang, og kan generere web- eller skrivebordsprogrammer i en rekke programmeringsspråk bare ved å trykke på en knapp som kan generere ønsket PSM-kode.
    Mange RAD-verktøy (Rapid Application Development) er opprettet basert på denne ideen: du tegner en modell og klikker på en knapp for å få en komplett applikasjon. Noen av disse verktøyene går så langt som å forsøke å fjerne utviklere helt fra ligningen der ikke-tekniske brukere antas å kunne gjøre trygge endringer i programvaren uten behov for en utvikler.

Jeg skulle også legge Objektrelasjonell kartlegging i listen som noen ORMer er sterkt avhengige av kodegenerering for å skape persistensmodellen fra en konseptuell eller fysisk datamodell. Jeg har brukt noen av disse verktøyene og har gjennomgått en god del smerte for å tilpasse den genererte koden. Med det sagt, synes mange utviklere å virkelig like dem, så jeg har nettopp forlatt det (eller gjorde jeg ?!);)

Mens noen av disse "verktøyene" løser noen av programmeringsproblemer og reduserer den nødvendige oppstarten og kostnaden for programvareutvikling, er det en enorm skjult vedlikeholdskostnad ved å bruke kodegenerering, som før eller senere skal bite deg og jo mer generert kode du har, jo mer at det kommer til å skade.

Jeg vet at mange utviklere er store fans av kodegenerering og skriver et nytt kodegenereringsskript hver dag. Hvis du er i den leiren og tror det er et flott verktøy for mange problemer, skal jeg ikke argumentere for deg. Tross alt, dette innlegget handler ikke om å bevise at kodegenerering er en dårlig idé.

Noen ganger kan kodegenerering bare være en god ide

Svært sjelden skjønt, finner jeg meg selv i en situasjon der kodegenerering passer godt for problemet ved hånden, og de alternative løsningene vil enten være vanskeligere eller ugligere.

Her er noen eksempler på hvor kodegenerering kan være en god passform:

  • Du må skrive mye boilerplate kode som følger et lignende statisk mønster. Før du prøver kodegenerering, bør du i så fall tenke veldig hardt om problemet, og prøv å skrive denne koden riktig (for eksempel ved å bruke objektorienterte mønstre hvis du skriver OO-kode). Hvis du har prøvd hardt og ikke har funnet en god løsning, kan kodegenerering være et godt valg.
  • Du bruker ofte statiske metadata fra en ressurs og henter dataene som krever bruk av magiske strenger (og kanskje en kostbar drift). Her er noen eksempler:
    • Kode metadata hentet av refleksjon: Ringekode ved hjelp av refleksjon krever magiske strenger; men i designtiden vet du hva du trenger, du kan bruke kodegenerering til å generere de nødvendige artefakter. På denne måten vil du unngå å bruke refleksjoner ved kjøretid og / eller magiske strenger i koden din. Et godt eksempel på dette konseptet er T4MVC som skaper sterke typede hjelpere som eliminerer bruken av bokstavsstrenge mange steder.
    • Statisk oppslagstjenester: nå og da kommer jeg over webtjenester som bare gir statiske data som kan hentes ved å gi en nøkkel, som ender opp som en magisk streng i kodebase. I dette tilfellet, hvis du kan programmatisk hente alle nøklene, kan du kode generere en statisk klasse som inneholder alle nøklene og få tilgang til strengverdiene som sterkt skrevet første klasse borgere i kodebasen din i stedet for å bruke magiske strenger. Du kan selvsagt lage klassen manuelt; men du må også opprettholde den manuelt hver gang dataene endres. Du kan deretter bruke denne klassen til å slå webtjenesten og cache resultatet slik at de påfølgende samtalene blir løst fra minnet.
      Alternativt, hvis tillatt, kan du bare generere hele tjenesten i koden, slik at oppslagstjenesten ikke kreves ved kjøring. Begge løsningene har noen fordeler og ulemper, så velg den som passer dine behov. Sistnevnte er bare nyttig hvis nøklene bare brukes av søknaden og ikke leveres av brukeren; ellers før eller senere blir det en tid da tjenestedataene har blitt oppdatert, men du har ikke generert koden, og brukerens initierte oppslag mislykkes.
    • Statiske oppslagstabeller: Dette ligner veldig på statiske webtjenester, men dataene lever i en datalager i motsetning til en webtjeneste.

Som nevnt ovenfor gjør kodegenerering for ufleksibel og vanskelig å vedlikeholde kode; så hvis arten av problemet du løser er statisk og ikke krever hyppig vedlikehold, kan kodegenerering være en god løsning!

Bare fordi problemet ditt passer inn i en av de ovennevnte kategoriene, betyr ikke at kodegenerering passer bra til det. Du bør fortsatt prøve å evaluere alternative løsninger og veie alternativene dine.

Også, hvis du går for kodegenerering, sørg for å fremdeles skrive enhetstester. Av noen grunn tror noen utviklere at generert kode ikke krever enhetstesting. Kanskje de tror det er generert av datamaskiner og datamaskiner gjør ikke feil! Jeg tror generert kode krever like mye (hvis ikke mer) automatisk verifisering. Jeg personlig TDD min kodegenerering: Jeg skriver testene først, kjører dem for å se dem mislykkes, så generer koden og se testene passere.

Tekstmaskettransformasjonsverktøyet

Det er en fantastisk kodegenereringsmotor i Visual Studio kalt Text Template Transformation Toolkit (AKA, T4).

Fra MSDN:

Tekstmaler er sammensatt av følgende deler:

  • direktiver: elementer som styrer hvordan malen behandles.
  • Tekstblokker: innhold som kopieres direkte til utgangen.
  • Kontrollblokker: Programkode som legger inn variable verdier i teksten og kontrollerer betingede eller gjentatte deler av teksten.

I stedet for å snakke om hvordan T4 fungerer, vil jeg gjerne bruke et reelt eksempel. Så her er et problem jeg møtte en stund tilbake som jeg brukte T4. Jeg har et åpen kildekode. NET-bibliotek kalt Humanizer. En av de tingene jeg ønsket å gi i Humanizer var en flytende utvikler vennlig API for å jobbe med Dato tid.

Jeg betraktet ganske mange varianter av API og på slutten avgjort:

In.January // Returns 1. januar i inneværende år In.FebruaryOf (2009) // Returnerer 1. februar 2009 On.January.The4th // Returnerer 4. januar i inneværende år On.February.The (12) // Returnerer 12. februar i inneværende år In.One.Second // DateTime.UtcNow.AddSeconds (1); In.Two.Minutes // Med tilsvarende Fra metode In.Three.Hours // Med tilsvarende Fra metode In.Five.Days // Med tilsvarende Fra metode In.Six.Weeks // Med tilsvarende Fra metode In.Seven.Months / / Med tilsvarende Fra metode In.ight.Years // Med tilsvarende Fra metode In.Two.SecondsFrom (DateTime dateTime)

Etter at jeg visste hva min API skulle se ut, tenkte jeg på noen forskjellige måter å takle dette på og spikte noen objektorienterte løsninger, men alle krevde en god bit av boilerplate-kode og de som ikke gjorde det, ville ikke gi meg den rene offentlige API som jeg ønsket. Så bestemte jeg meg for å gå med kodegenerering.

For hver variant opprettet jeg en egen T4-fil:

  • In.Months.tt for I januar og In.FebrurayOf () og så videre.
  • On.Days.tt for On.January.The4th, On.February.The (12) og så videre.
  • In.SomeTimeFrom.tt for In.One.Second, In.TwoSecondsFrom (), In.Three.Minutes og så videre.

Her skal jeg diskutere On.Days. Koden er kopiert her for din referanse:

<#@ 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" #> bruker system; namespace Humanizer offentlig delklasse på  <# const int leapYear = 2012; for (int month = 1; month <= 12; month++)  var firstDayOfMonth = new DateTime(leapYear, month, 1); var monthName = firstDayOfMonth.ToString("MMMM");#> ///  /// Gir flytende datatilgang for <#= monthName #> ///  offentlig klasse <#= monthName #> ///  / / Den neste dagen av <#= monthName #> i inneværende år ///  offentlig statisk datotid (int dagnummer) returner ny datotid (datotid.når.år, <#= month #>, dayNumber);  <#for (int day = 1; day <= DateTime.DaysInMonth(leapYear, month); day++)  var ordinalDay = day.Ordinalize();#> ///  /// The <#= ordinalDay #> dag av <#= monthName #> i inneværende år ///  offentlig statisk datatid<#= ordinalDay #> get returner ny DateTime (DateTime.Now.Year, <#= month #>, <#= day #>);  <##>  <##> 

Hvis du sjekker denne koden i Visual Studio eller vil jobbe med T4, må du kontrollere at du har installert Tangible T4 Editor for Visual Studio. Den gir IntelliSense, T4 Syntax-Highlighting, Avansert T4 Debugger og T4 Transform on Build.

Koden kan virke litt skummel i begynnelsen, men det er bare et skript som ligner på ASP-språket. Ved lagring vil dette generere en klasse som kalles med 12 underklasser, en per måned (for eksempel, januar, februar etc) hver med offentlige statiske egenskaper som returnerer en bestemt dag i den måneden. La oss slå koden fra hverandre og se hvordan det fungerer.

direktiver

Direktivets syntaks er som følger: <#@ DirectiveName [AttributeName = "AttributeValue"]… #>. Du kan lese mer om direktiver her.

Jeg har brukt følgende direktiver i koden:

Mal

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

Maledirektivet har flere attributter som gir deg mulighet til å spesifisere ulike aspekter av transformasjonen.

Hvis debug attributtet er ekte, Mellomkodefilen inneholder informasjon som gjør det mulig for debuggeren å identifisere posisjonen i malen mer nøyaktig hvor det oppstod en pause eller et unntak. Jeg forlater dette alltid som ekte.

Produksjon

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

Utgangsdirektivet brukes til å definere filnavnets utvidelse og koding av den transformerte filen. Her setter vi utvidelsen til .cs som betyr at den genererte filen vil være i C # og filnavnet vil være On.Days.cs.

montering

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

Her laster vi inn System.Core slik at vi kan bruke den i kodeblokkene lenger nede.

Forsamlingsdirektivet laster inn en samling slik at malekoden kan bruke sine typer. Effekten ligner på å legge til en monteringsreferanse i et Visual Studio-prosjekt.

Dette betyr at du kan dra full nytte av .NET-rammen i T4-malen. For eksempel kan du bruke ADO.NET til å treffe en database, lese noen data fra et bord og bruke det til kodegenerering.

Videre ned, har jeg følgende linje:

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

Dette er litt interessant. I On.Days.tt mal Jeg bruker Ordinalize-metoden fra Humanizer som gjør et tall til en ordinært streng, som brukes til å betegne stillingen i en bestilt rekkefølge som 1., 2., 3., 4., Dette brukes til å generere the1st, The2nd og så videre.

Fra MSDN-artikkelen:

Monteringsnavnet skal være ett av følgende:

  • Det sterke navnet på en forsamling i GAC, for eksempel system.xml.dll. Du kan også bruke den lange formen, for eksempel navn = "System.Xml, Version = 4.0.0.0, Kultur = nøytral, PublicKeyToken = b77a5c561934e089". For mer informasjon, se AssemblyName.
  • Den absolutte stien til forsamlingen.

System.Core lever i GAC, så vi kunne bare enkelt bruke navnet hans; men for humanizer må vi gi den absolutte banen. Tydeligvis vil jeg ikke hardkode min lokale sti, så jeg brukte $ (SolutionDir) som erstattes av stien løsningen lever i under kodegenerering. På denne måten fungerer kodegenerering bra for alle, uansett hvor de holder koden.

Importere

<#@ import namespace="System" #>

Importdirektivet lar deg referere til elementer i et annet navneområde uten å gi et fullt kvalifisert navn. Det er ekvivalent av ved hjelp av setning i C # eller import i Visual Basic.

På toppen definerer vi alle navneområder vi trenger i kodeblokkene. De importere blokkerer du ser det er mest sett inn av T4 Tangible. Det eneste jeg la til var:

<#@ import namespace="Humanizer" #> 

Så jeg kan senere skrive:

var ordinalDay = day.Ordinalize (); 

Uten importere uttalelse og spesifisering av montering ved sti, i stedet for en C # -fil, ville jeg ha fått en kompileringsfeil som klaget over ikke å finne Ordinalize metode på heltall.

Tekstblokker

En tekstblokk legger inn tekst direkte i utdatafilen. På toppen har jeg skrevet noen linjer med C # -koden som kopieres direkte til den genererte filen:

bruker system; namespace Humanizer offentlig delklasse på 

Lenger nede, i mellom kontrollblokker, har jeg andre tekstblokker for API-dokumentasjon, metoder og også for lukking av parenteser.

Kontrollblokker

Kontrollblokker er deler av programkoden som brukes til å transformere malene. Standardspråket er C #.

Merk: Språket der du skriver koden i kontrollblokkene, er ikke relatert til språket i teksten som genereres.

Det finnes tre ulike typer kontrollblokker: Standard, Expression og Class Feature. 

Fra MSDN:

  • <# Standard control blocks #> kan inneholde uttalelser.
  • <#= Expression control blocks #> kan inneholde uttrykk.
  • <#+ Class feature control blocks #> kan inneholde metoder, felt og egenskaper.

La oss ta en titt på kontrollblokker som vi har i eksempelmalen:

<# const int leapYear = 2012; for (int month = 1; month <= 12; month++)  var firstDayOfMonth = new DateTime(leapYear, month, 1); var monthName = firstDayOfMonth.ToString("MMMM");#> ///  /// Gir flytende datatilgang for <#= monthName #> ///  offentlig klasse <#= monthName #> ///  / / Den neste dagen av <#= monthName #> i inneværende år ///  offentlig statisk datotid (int dagnummer) returner ny datotid (datotid.når.år, <#= month #>, dayNumber);  <#for (int day = 1; day <= DateTime.DaysInMonth(leapYear, month); day++)  var ordinalDay = day.Ordinalize();#> ///  /// The <#= ordinalDay #> dag av <#= monthName #> i inneværende år ///  offentlig statisk datatid<#= ordinalDay #> get returner ny DateTime (DateTime.Now.Year, <#= month #>, <#= day #>);  <##>  <##>

For meg personlig er det mest forvirrende tingen om T4 å åpne og lukke kontrollblokkene, da de ganske enkelt blir blandet med brakettene i tekstblokken (hvis du genererer kode for et krøllete brakettspråk som C #). Jeg finner den enkleste måten å håndtere dette, er å lukke (#>) kontrollblokken så snart jeg åpner (<#) det og skriv deretter koden innvendig.

På toppen, inne i standard kontrollblokken, definerer jeg skuddår som en konstant verdi. Dette er slik at jeg kan generere en oppføring for 29. februar. Deretter går det over 12 måneder for hver måned å få firstDayOfMonth og monthName. Jeg lukker deretter kontrollblokken for å skrive en tekstblokk for månedsklassen og XML-dokumentasjonen. De monthName brukes som et klassenavn og i XML-kommentarer (ved hjelp av uttrykkskontrollblokker). Resten er bare vanlig C # kode som jeg ikke skal bore deg med.

Konklusjon

I dette innlegget snakket jeg om kodegenerering, forutsatt noen eksempler på når kodegenerering kan være enten farlig eller nyttig, og også vist hvordan du kan bruke T4-maler til å generere kode fra Visual Studio ved hjelp av et reelt eksempel.

Hvis du vil lære mer om T4, kan du finne mye godt innhold på Oleg Sychs blogg.