Nu îmi place generarea de coduri și, de obicei, o văd ca pe un "miros". Dacă utilizați generarea de coduri de orice fel, există o șansă bună ca ceva să fie în neregulă cu designul sau soluția! Deci, probabil, în loc să scrieți un script pentru a genera mii de linii de cod, trebuie să faceți un pas înapoi, să vă gândiți din nou la problema dvs. și să găsiți o soluție mai bună. Odată cu aceasta, există situații în care generarea de coduri ar putea fi o soluție bună.
În acest post, voi vorbi despre avantajele și dezavantajele generării de coduri și apoi vă vom arăta cum să folosiți șabloanele T4, instrumentul de generare a codurilor încorporate în Visual Studio, folosind un exemplu.
Scriu un post despre un concept care cred că este o idee rea, mai des decât nu și ar fi neprofesionist de mine dacă ți-aș înmâna un instrument și nu te-am avertizat de pericolele lui.
Adevărul este că generarea de coduri este destul de interesantă: scrieți câteva linii de cod și obțineți mult mai mult, în schimb că ar trebui să scrieți manual. Deci, este ușor să intrăm într-o capcană unică pentru toți:
"Dacă singurul instrument pe care îl aveți este un ciocan, aveți tendința de a vedea fiecare problemă ca un cui" ". A. Maslow
Dar generarea de coduri este aproape întotdeauna o idee proastă. Te refer la acest post, care explică majoritatea problemelor pe care le văd cu generarea de coduri. Pe scurt, generarea de coduri are drept rezultat un cod inflexibil și greu de întreținut.
Iată câteva exemple de unde ar trebui nu generați codul de utilizare:
De asemenea, aveam de gând să pun Object Relational Mapping în listă deoarece unele ORM se bazează foarte mult pe generarea de coduri pentru a crea modelul de persistență dintr-un model conceptual sau fizic de date. Am folosit câteva dintre aceste instrumente și am suferit un pic de durere pentru a personaliza codul generat. Cu acest lucru a spus, o mulțime de dezvoltatori par să le place foarte mult, așa că am lăsat doar asta (sau am ?!);)
În timp ce unele dintre aceste "unelte" rezolvă unele dintre problemele de programare și reduc efortul necesar în avans și costul dezvoltării software-ului, există un mare cost de întreținere ascuns în utilizarea generării de coduri, că mai devreme sau mai târziu vă va mușca și cu atât mai mult generat de codul pe care îl aveți, cu atât mai mult va face rău.
Știu că o mulțime de dezvoltatori sunt fani imense de generare de coduri și scriu un nou script de generare de coduri în fiecare zi. Dacă vă aflați în acea tabără și credeți că este un instrument excelent pentru o mulțime de probleme, nu voi contesta cu voi. La urma urmei, această postare nu este despre dovedirea generării de coduri este o idee rea.
Foarte rar, însă, mă găsesc într-o situație în care generarea de coduri este potrivită pentru problema la îndemână și soluțiile alternative ar fi fie mai grele, fie mai dură.
Iată câteva exemple de generare de coduri care ar putea fi potrivite:
După cum am menționat mai sus, generarea de coduri face ca un cod inflexibil și greu de întreținut; astfel încât, dacă natura problemei pe care o rezolviți este statică și nu necesită întreținere frecventă, atunci generarea de coduri ar putea fi o soluție bună!
Doar pentru că problema dvs. se încadrează într-una din categoriile de mai sus nu înseamnă că generarea de coduri este o potrivire bună pentru aceasta. Încă ar trebui să încercați să evaluați soluțiile alternative și să vă evaluați opțiunile.
De asemenea, dacă utilizați generarea de coduri, asigurați-vă că încă mai scrieți teste unitate. Din anumite motive, unii dezvoltatori consideră că codul generat nu necesită testarea unităților. Poate că ei cred că este generat de computere și computere nu fac greșeli! Cred că codul generat necesită o verificare automatizată la fel de mult (dacă nu mai mult). Eu personal TDD generarea de codul meu: am scris testele mai întâi, rulați-le pentru a le vedea nu reușesc, apoi generați codul și a vedea testele trece.
Există un motor de generare de cod minunat în Visual Studio numit Text Template Transformation Toolkit (AKA, T4).
De la MSDN:
Șabloanele de text sunt compuse din următoarele părți:
În loc să vorbim despre cum funcționează T4, aș dori să folosesc un exemplu real. Deci, aici este o problema cu care m-am confruntat un timp inapoi pentru care am folosit T4. Am o bibliotecă open source .NET numită Humanizer. Unul dintre lucrurile pe care am vrut să le furnizez în Humanizer a fost un API prietenos pentru dezvoltatori, cu care să lucrez DateTime
.
Am analizat câteva variații ale API și, în cele din urmă, am stabilit acest lucru:
In.January // Retururi 1 ianuarie a anului curent In.FebruaryOf (2009) // Retururi 1 februarie 2009 On.January.The 4th // Intoarce 4 ianuarie a anului curent On.February.The (12) // Returnează data de 12 februarie a anului curent In.One.Second // DateTime.UtcNow.AddSeconds (1); In.Two.Minute // Cu metoda Din In.Three.Hours corespunzatoare // Cu metoda Din In.Five.Desa corespunzatoare Din // Cu metoda Din In.Six.Weeks corespunzatoare // Cu metoda Din In.Seven.Months / / Cu metoda corespunzătoare In.Eight.Years / Cu metoda Din In.Two.SecondsFrom (DateTime dateTime)
După ce am știut ce ar arăta API-ul meu ca m-am gândit la câteva moduri diferite de a aborda acest lucru și a împărțit câteva soluții orientate pe obiecte, dar toate au nevoie de un pic echitabil de cod de boilerplate și cei care nu au făcut-o, nu ar dă-mi API publică curată pe care am vrut-o. Așa că am decis să merg cu generarea de coduri.
Pentru fiecare variație am creat un fișier separat T4:
În ianuarie
și In.FebrurayOf ()
si asa mai departe.On.January.The4th
, On.February.The (12)
si asa mai departe. In.One.Second
, In.TwoSecondsFrom ()
, In.Three.Minutes
si asa mai departe. Aici voi discuta On.Days
. Codul este copiat aici pentru referință:
<#@ 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" #> utilizând Sistemul; namespace Humanizer clasa publică parțială La <# const int leapYear = 2012; for (int month = 1; month <= 12; month++) var firstDayOfMonth = new DateTime(leapYear, month, 1); var monthName = firstDayOfMonth.ToString("MMMM");#> ////// Oferă accesori fluent pentru data de <#= monthName #> /// clasa publică <#= monthName #> ////// Ziua n <#= monthName #> din anul curent /// public static DateTime (int dayNumber) întoarce noua DateTime (DateTime.Now.Year, <#= month #>, dayNumber); <#for (int day = 1; day <= DateTime.DaysInMonth(leapYear, month); day++) var ordinalDay = day.Ordinalize();#> ////// The <#= ordinalDay #> ziua de <#= monthName #> din anul curent /// public static<#= ordinalDay #> get returnați noul DateTime (DateTime.Now.Year, <#= month #>, <#= day #>); <##> <##>
Dacă verificați acest cod în Visual Studio sau doriți să lucrați cu T4, asigurați-vă că ați instalat Tangible T4 Editor pentru Visual Studio. Oferă IntelliSense, T4 Syntax-Highlighting, Advanced T4 Debugger și T4 Transform on Build.
Codul ar putea părea un pic înfricoșător la început, dar este doar un script foarte asemănător cu limbajul ASP. După salvare, aceasta va genera o clasă numită Pe
cu 12 subclase, câte unul pe lună (de ex, ianuarie
, februarie
etc) fiecare cu proprietăți statice publice care returnează o anumită zi în acea lună. Să distrugem codul separat și să vedem cum funcționează.
Sintaxa directivelor este după cum urmează: <#@ DirectiveName [AttributeName = "AttributeValue"]… #>
. Puteți citi mai multe despre directive aici.
Am folosit următoarele coduri în cod:
<#@ template debug="true" hostSpecific="true" #>
Directiva șablon are mai multe atribute care vă permit să specificați diferite aspecte ale transformării.
În cazul în care depanare
este atributul Adevărat
, fișierul de cod intermediar va conține informații care permit debuggerului să identifice cu mai multă precizie poziția din șablonul în care a avut loc o pauză sau o excepție. Întotdeauna lăsăm asta Adevărat
.
<#@ output extension=".cs" #>
Direcția de ieșire este utilizată pentru a defini extensia de nume de fișier și codarea fișierului transformat. Aici am setat extensia la .cs
ceea ce înseamnă că fișierul generat va fi în C #, iar numele fișierului va fi On.Days.cs
.
<#@ assembly Name="System.Core" #>
Aici încărcăm System.Core
astfel încât să putem folosi în blocurile de coduri în continuare.
Directiva directivei Adunării încarcă un ansamblu astfel încât codul șablonului să poată utiliza tipurile sale. Efectul este similar cu adăugarea unei referințe de asamblare într-un proiect Visual Studio.
Aceasta înseamnă că puteți profita din plin de cadrul .NET în șablonul dvs. T4. De exemplu, puteți utiliza ADO.NET pentru a lovi o bază de date, pentru a citi unele date dintr-un tabel și a le folosi pentru generarea de coduri.
Mai jos, am următoarea linie:
<#@ assembly name="$(SolutionDir)Humanizer\bin\Debug\Humanizer.dll" #>
E un pic interesant. În On.Days.tt
șablon Folosesc metoda Ordinalize de la Humanizer, care transformă un număr într-un șir ordinal, folosit pentru a indica poziția într-o ordine ordonată, cum ar fi 1, 2, 3, 4. Acesta este folosit pentru a genera The1st
, The2nd
si asa mai departe.
Din articolul MSDN:
Numele ansamblului ar trebui să fie unul dintre următoarele:
System.Xml.dll
. De asemenea, puteți utiliza formularul lung, cum ar fi name = "System.Xml, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089". Pentru mai multe informații, vedeți AssemblyName
.System.Core
trăiește în GAC, așa că am putea să-i folosim cu ușurință numele; dar pentru Humanizer trebuie să oferim calea absolută. Evident, nu vreau să-mi scriu hardcodul, așa că am folosit-o $ (SolutionDir)
care este înlocuită de calea pe care o trăiește soluția în timpul generării codului. În acest fel, generarea de coduri funcționează bine pentru toată lumea, indiferent de locul în care păstrează codul.
<#@ import namespace="System" #>
Directiva de import vă permite să vă referiți la elemente dintr-un alt spațiu de nume fără a oferi un nume complet calificat. Este echivalentul lui utilizând
declarație în C # sau importuri
în Visual Basic.
În partea superioară definim toate spațiile de nume de care avem nevoie în blocurile de coduri. import
blocurile pe care le vedeți că sunt introduse mai ales de T4 Tangible. Singurul lucru pe care l-am adăugat a fost:
<#@ import namespace="Humanizer" #>
Deci pot scrie mai târziu:
var ordinalDay = zi.Ordinize ();
Fara import
declarație și specificând asamblare
pe cale, în loc de un fișier C #, aș fi obținut o eroare de compilare care se plânge că nu a găsit Ordinalize
metoda pe întreg.
Un bloc de text introduce text direct în fișierul de ieșire. În partea de sus, am scris câteva linii de cod C # care se copiază direct în fișierul generat:
utilizând Sistemul; namespace Humanizer clasa publică parțială La
Mai departe, între blocurile de control, am câteva blocuri de text pentru documentația API, metode și, de asemenea, pentru închiderea parantezelor.
Blocurile de control sunt secțiuni ale codului programului care sunt utilizate pentru a transforma șabloanele. Limba implicită este C #.
Notă: Limba în care scrieți codul în blocurile de control nu are legătură cu limba textului generat.
Există trei tipuri diferite de blocuri de control: Standard, Expression și Class Feature.
De la MSDN:
<# Standard control blocks #>
pot conține declarații.<#= Expression control blocks #>
pot conține expresii.<#+ Class feature control blocks #>
pot conține metode, câmpuri și proprietăți.Să aruncăm o privire asupra blocurilor de control pe care le avem în șablonul de eșantion:
<# const int leapYear = 2012; for (int month = 1; month <= 12; month++) var firstDayOfMonth = new DateTime(leapYear, month, 1); var monthName = firstDayOfMonth.ToString("MMMM");#> ////// Oferă accesori fluent pentru data de <#= monthName #> /// clasa publică <#= monthName #> ////// Ziua n <#= monthName #> din anul curent /// public static DateTime (int dayNumber) întoarce noua DateTime (DateTime.Now.Year, <#= month #>, dayNumber); <#for (int day = 1; day <= DateTime.DaysInMonth(leapYear, month); day++) var ordinalDay = day.Ordinalize();#> ////// The <#= ordinalDay #> ziua de <#= monthName #> din anul curent /// public static<#= ordinalDay #> get returnați noul DateTime (DateTime.Now.Year, <#= month #>, <#= day #>); <##> <##>
Pentru mine personal, cel mai confuz cu privire la T4 este blocurile de control de deschidere și închidere, deoarece se amestecă destul de puțin cu parantezele din blocul de text (dacă generezi cod pentru un limbaj curbat, cum ar fi C #). Consider că cel mai ușor mod de a face față acestui lucru este închiderea (#>
) blocul de control de îndată ce deschid (<#
) și apoi scrieți codul înăuntru.
Pe partea de sus, în interiorul blocului standard de control, definesc leapYear
ca valoare constantă. Acest lucru este posibil pentru a genera o intrare pentru 29 februarie. Apoi am itera peste 12 luni pentru fiecare lună obtinerea firstDayOfMonth
si MONTHNAME
. Apoi închid blocul de control pentru a scrie un bloc de text pentru clasa de luni și documentația sa XML. MONTHNAME
este folosit ca un nume de clasă și în comentarii XML (folosind blocuri de control al exprimării). Restul este doar codul normal C # cu care nu te voi plictisi.
În acest post am vorbit despre generarea de coduri, cu condiția ca câteva exemple de generare de coduri care ar putea fi fie periculoase sau utile și au arătat și cum puteți utiliza șabloanele T4 pentru a genera coduri din Visual Studio utilizând un exemplu real.
Dacă doriți să aflați mai multe despre T4, puteți găsi un conținut extraordinar pe blogul lui Oleg Sych.