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.
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:
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é.
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:
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.
Det er en fantastisk kodegenereringsmotor i Visual Studio kalt Text Template Transformation Toolkit (AKA, T4).
Fra MSDN:
Tekstmaler er sammensatt av følgende deler:
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:
I januar
og In.FebrurayOf ()
og så videre.On.January.The4th
, On.February.The (12)
og så videre. 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 På
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.
Direktivets syntaks er som følger: <#@ DirectiveName [AttributeName = "AttributeValue"]… #>
. Du kan lese mer om direktiver her.
Jeg har brukt følgende direktiver i koden:
<#@ 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
.
<#@ 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
.
<#@ 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:
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
.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.
<#@ 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.
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 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.
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.