Non mi piace la generazione del codice e di solito, la vedo come un "odore". Se stai usando una generazione di codice di qualsiasi tipo, c'è una buona probabilità che qualcosa non funzioni nel tuo progetto o soluzione! Quindi, forse, invece di scrivere uno script per generare migliaia di righe di codice, dovresti fare un passo indietro, ripensare al tuo problema e trovare una soluzione migliore. Detto questo, ci sono situazioni in cui la generazione del codice potrebbe essere una buona soluzione.
In questo post parlerò di pro e contro della generazione del codice e poi mostrerò come usare i template T4, lo strumento di generazione del codice integrato in Visual Studio, usando un esempio.
Sto scrivendo un post su un concetto che penso sia una cattiva idea, il più delle volte e sarebbe poco professionale da parte mia se ti consegnassi uno strumento e non ti avessi avvisato dei suoi pericoli.
La verità è che la generazione del codice è piuttosto eccitante: scrivi poche righe di codice e ottieni Un sacco più in cambio che forse dovresti scrivere manualmente. Quindi è facile cadere in una trappola "taglia unica" con esso:
"Se l'unico strumento che hai è un martello, tendi a vedere ogni problema come un chiodo" ". A. Maslow
Ma la generazione del codice è quasi sempre una cattiva idea. Ti rimando a questo post, che spiega la maggior parte dei problemi che vedo con la generazione del codice. In poche parole, la generazione del codice risulta in un codice inflessibile e difficile da mantenere.
Ecco alcuni esempi di dove dovresti non usa la generazione del codice:
Inserirò anche l'Object Relational Mapping nella lista poiché alcuni ORM fanno molto affidamento sulla generazione del codice per creare il modello di persistenza da un modello di dati concettuale o fisico. Ho usato alcuni di questi strumenti e ho subito un bel po 'di dolore per personalizzare il codice generato. Detto questo, molti sviluppatori sembrano apprezzarli davvero, quindi l'ho lasciato fuori (o ho fatto ?!);)
Mentre alcuni di questi "strumenti" risolvono alcuni dei problemi di programmazione e riducono lo sforzo iniziale e il costo dello sviluppo del software, vi è un enorme costo nascosto per la manutenibilità nell'uso della generazione di codice, che prima o poi morderà e più codice generato che hai, più che farà male.
So che molti sviluppatori sono grandi fan della generazione del codice e scrivono ogni giorno un nuovo script di generazione del codice. Se sei in quel campo e pensi che sia un ottimo strumento per molti problemi, non ho intenzione di discutere con te. Dopotutto, questo post non vuole dimostrare che la generazione del codice è una cattiva idea.
Molto raramente, però, mi trovo in una situazione in cui la generazione del codice è adatta al problema in questione e le soluzioni alternative potrebbero essere più difficili o più brutte.
Ecco alcuni esempi di dove la generazione del codice potrebbe essere una buona idea:
Come accennato in precedenza, la generazione del codice rende il codice inflessibile e difficile da gestire; quindi se la natura del problema che stai risolvendo è statica e non richiede una manutenzione frequente, la generazione del codice potrebbe essere una buona soluzione!
Solo perché il tuo problema rientra in una delle categorie di cui sopra non significa che la generazione del codice sia adatta per questo. Dovresti comunque provare a valutare soluzioni alternative e valutare le tue opzioni.
Inoltre, se si utilizza la generazione del codice, assicurarsi di scrivere ancora i test unitari. Per qualche motivo, alcuni sviluppatori pensano che il codice generato non richieda il test delle unità. Forse pensano che sia generato da computer e computer non commettono errori! Penso che il codice generato richieda altrettante (se non più) verifiche automatizzate. Io personalmente TDD la mia generazione di codice: scrivo prima i test, li eseguo per vederli fallire, quindi generare il codice e vedere passare i test.
C'è un fantastico motore di generazione del codice in Visual Studio chiamato Text Template Transformation Toolkit (AKA, T4).
Da MSDN:
I modelli di testo sono composti dalle seguenti parti:
Invece di parlare di come funziona T4, mi piacerebbe usare un esempio reale. Quindi ecco un problema che ho affrontato un po 'indietro per il quale ho usato T4. Ho una libreria .NET open source chiamata Humanizer. Una delle cose che volevo fornire in Humanizer era una API fluente per sviluppatori amichevole con cui lavorare Appuntamento
.
Ho preso in considerazione alcune varianti dell'API e alla fine ho deciso per questo:
In. Gennaio // Ritorna il 1 ° gennaio dell'anno in corso In.FebruaryOf (2009) // Resi il 1 ° febbraio 2009 On.January.The4th // Restituisce il 4 gennaio dell'anno corrente On.February.The (12) // Restituisce il 12 febbraio dell'anno corrente In.One.Second // DateTime.UtcNow.AddSeconds (1); In.Two.Minutes // Con il metodo From corrispondente In.Three.Hours // Con il metodo From corrispondente In.Five.Days // Con il metodo From corrispondente In.Six.Weeks // Con il metodo From corrispondente In.Seven.Months / / Con il metodo Da corrispondente In.Eight.Years // Con il metodo From corrispondente In.Two.SecondsFrom (DateTime dateTime)
Dopo aver saputo che aspetto avrebbe avuto la mia API, ho pensato ad alcuni modi diversi per affrontare questo problema e ho risolto alcune soluzioni orientate agli oggetti, ma tutti richiedevano un bel po 'di codice boilerplate e quelli che non lo facevano, non lo sarebbero dammi l'API pubblica pulita che volevo. Così ho deciso di andare con la generazione del codice.
Per ogni variazione ho creato un file T4 separato:
A gennaio
e In.FebrurayOf ()
e così via.On.January.The4th
, On.February.The (12)
e così via. In.One.Second
, In.TwoSecondsFrom ()
, In.Three.Minutes
e così via. Qui discuterò On.Days
. Il codice è copiato qui per il vostro riferimento:
<#@ 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" #> usando il sistema; namespace 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");#> ////// Fornisce accessor data fluente per <#= monthName #> /// classe pubblica <#= monthName #> ////// L'ennesimo giorno di <#= monthName #> dell'anno in corso /// public static DateTime The (int dayNumber) return new DateTime (DateTime.Now.Year, <#= month #>, dayNumber); <#for (int day = 1; day <= DateTime.DaysInMonth(leapYear, month); day++) var ordinalDay = day.Ordinalize();#> ////// Il <#= ordinalDay #> giorno di <#= monthName #> dell'anno in corso /// Date statiche pubbliche<#= ordinalDay #> get return new DateTime (DateTime.Now.Year, <#= month #>, <#= day #>); <##> <##>
Se stai verificando questo codice in Visual Studio o vuoi lavorare con T4, assicurati di aver installato Tangible T4 Editor per Visual Studio. Fornisce IntelliSense, T4 Sintassi-Evidenziamento, Advanced T4 Debugger e T4 Transform on Build.
Il codice potrebbe sembrare un po 'spaventoso all'inizio, ma è solo uno script molto simile al linguaggio ASP. Al momento del salvataggio, verrà generata una classe chiamata Sopra
con 12 sottoclassi, una al mese (per esempio, gennaio
, febbraio
ecc.) ciascuno con proprietà statiche pubbliche che restituiscono un giorno specifico in quel mese. Rompiamo il codice e vediamo come funziona.
La sintassi delle direttive è la seguente: <#@ DirectiveName [AttributeName = "AttributeValue"]… #>
. Puoi leggere di più sulle direttive qui.
Ho usato le seguenti direttive nel codice:
<#@ template debug="true" hostSpecific="true" #>
La direttiva Modello ha diversi attributi che consentono di specificare diversi aspetti della trasformazione.
Se la mettere a punto
l'attributo è vero
, il file di codice intermedio conterrà informazioni che consentono al debugger di identificare con maggiore precisione la posizione nel modello in cui si è verificata un'interruzione o un'eccezione. Lascio sempre questo come vero
.
<#@ output extension=".cs" #>
La direttiva Output viene utilizzata per definire l'estensione del nome del file e la codifica del file trasformato. Qui impostiamo l'estensione a .cs
il che significa che il file generato sarà in C # e il nome del file sarà On.Days.cs
.
<#@ assembly Name="System.Core" #>
Qui stiamo caricando System.Core
quindi possiamo usarlo nei blocchi di codice più in basso.
La direttiva Assembly carica un assembly in modo che il codice del modello possa utilizzarne i tipi. L'effetto è simile all'aggiunta di un riferimento all'assembly in un progetto di Visual Studio.
Ciò significa che puoi sfruttare appieno il framework .NET nel tuo modello T4. Ad esempio, è possibile utilizzare ADO.NET per colpire un database, leggere alcuni dati da una tabella e utilizzarli per la generazione del codice.
Più in basso, ho la seguente riga:
<#@ assembly name="$(SolutionDir)Humanizer\bin\Debug\Humanizer.dll" #>
Questo è un po 'interessante. Nel On.Days.tt
template Sto usando il metodo Ordinalize di Humanizer che trasforma un numero in una stringa ordinale, usata per denotare la posizione in una sequenza ordinata come 1a, 2a, 3a, 4a. Questo è usato per generare the1st
, The2nd
e così via.
Dall'articolo MSDN:
Il nome dell'assembly dovrebbe essere uno dei seguenti:
System.Xml.dll
. È inoltre possibile utilizzare il modulo lungo, ad esempio name = "System.Xml, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089". Per ulteriori informazioni, vedere AssemblyName
.System.Core
vive in GAC, quindi potremmo semplicemente usare il suo nome; ma per Humanizer dobbiamo fornire il percorso assoluto. Ovviamente non voglio hardcode il mio percorso locale, quindi l'ho usato $ (SolutionDir)
che viene sostituito dal percorso in cui la soluzione vive durante la generazione del codice. In questo modo la generazione del codice funziona bene per tutti, indipendentemente da dove mantengono il codice.
<#@ import namespace="System" #>
La direttiva di importazione consente di fare riferimento a elementi in un altro spazio dei nomi senza fornire un nome completo. È l'equivalente del utilizzando
dichiarazione in C # o importazioni
in Visual Basic.
In cima stiamo definendo tutti i namespace di cui abbiamo bisogno nei blocchi di codice. Il importare
i blocchi che vedi sono per lo più inseriti da T4 Tangible. L'unica cosa che ho aggiunto è stata:
<#@ import namespace="Humanizer" #>
Quindi posso scrivere più tardi:
var ordinalDay = day.Ordinalize ();
Senza il importare
dichiarazione e specificando il montaggio
per path, invece di un file C #, avrei ottenuto un errore di compilazione lamentandomi di non aver trovato il file Ordinalize
metodo su intero.
Un blocco di testo inserisce il testo direttamente nel file di output. In cima, ho scritto alcune righe di codice C # che vengono copiate direttamente nel file generato:
usando il sistema; namespace Humanizer public partial class On
Più in basso, tra i blocchi di controllo, ho altri blocchi di testo per la documentazione API, i metodi e anche per le parentesi di chiusura.
I blocchi di controllo sono sezioni del codice del programma che vengono utilizzate per trasformare i modelli. La lingua predefinita è C #.
Nota: La lingua in cui si scrive il codice nei blocchi di controllo non è correlata alla lingua del testo generato.
Esistono tre diversi tipi di blocchi di controllo: Standard, Espressione e Caratteristica di classe.
Da MSDN:
<# Standard control blocks #>
può contenere dichiarazioni.<#= Expression control blocks #>
può contenere espressioni.<#+ Class feature control blocks #>
può contenere metodi, campi e proprietà.Diamo un'occhiata ai blocchi di controlli che abbiamo nel modello di esempio:
<# const int leapYear = 2012; for (int month = 1; month <= 12; month++) var firstDayOfMonth = new DateTime(leapYear, month, 1); var monthName = firstDayOfMonth.ToString("MMMM");#> ////// Fornisce accessor data fluente per <#= monthName #> /// classe pubblica <#= monthName #> ////// L'ennesimo giorno di <#= monthName #> dell'anno in corso /// public static DateTime The (int dayNumber) return new DateTime (DateTime.Now.Year, <#= month #>, dayNumber); <#for (int day = 1; day <= DateTime.DaysInMonth(leapYear, month); day++) var ordinalDay = day.Ordinalize();#> ////// Il <#= ordinalDay #> giorno di <#= monthName #> dell'anno in corso /// Date statiche pubbliche<#= ordinalDay #> get return new DateTime (DateTime.Now.Year, <#= month #>, <#= day #>); <##> <##>
Per me personalmente, la cosa più confusa di T4 sono i blocchi di controllo di apertura e chiusura, in quanto vengono mescolati con le parentesi nel blocco di testo (se stai generando codice per un linguaggio a parentesi graffe come C #). Trovo il modo più semplice per affrontare questo, è quello di chiudere (#>
) il blocco di controllo non appena apro (<#
) e poi scrivere il codice all'interno.
In alto, all'interno del blocco di controllo standard, sto definendo anno bisestile
come un valore costante. Questo è così che posso generare una voce per il 29 febbraio. Quindi eseguo un'iterazione per 12 mesi per ogni mese ricevendo il firstDayOfMonth
e il monthName
. Quindi chiudo il blocco di controllo per scrivere un blocco di testo per la classe del mese e la sua documentazione XML. Il monthName
è usato come nome di classe e nei commenti XML (usando i blocchi di controllo dell'espressione). Il resto è solo normale codice C # che non ti annoierò con.
In questo post ho parlato della generazione del codice, fornendo alcuni esempi di quando la generazione del codice potrebbe essere pericolosa o utile e ha anche mostrato come è possibile utilizzare i modelli T4 per generare codice da Visual Studio utilizzando un esempio reale.
Se vuoi saperne di più su T4, puoi trovare molti contenuti interessanti sul blog di Oleg Sych.