() translation by (you can also view the original English article)
Ich mag keine Codegenerierung und normalerweise sehe ich es als "Geruch". Wenn Sie Codegenerierung jeglicher Art verwenden, besteht eine gute Chance, dass etwas mit Ihrem Design oder Ihrer Lösung nicht stimmt! Anstatt also ein Skript zu schreiben, um Tausende von Codezeilen zu generieren, sollten Sie einen Schritt zurücktreten, erneut über Ihr Problem nachdenken und eine bessere Lösung finden. Trotzdem gibt es Situationen, in denen die Codegenerierung eine gute Lösung sein könnte.
In diesem Beitrag werde ich anhand eines Beispiels über die Vor- und Nachteile der Codegenerierung sprechen und Ihnen dann zeigen, wie Sie T4-Vorlagen, das in Visual Studio integrierte Tool zur Codegenerierung, verwenden.
Codegenerierung ist eine schlechte Idee
Ich schreibe einen Beitrag über ein Konzept, das ich meistens für eine schlechte Idee halte, und es wäre unprofessionell für mich, wenn ich Ihnen ein Werkzeug geben und Sie nicht vor seinen Gefahren warnen würde.
Die Wahrheit ist, dass die Codegenerierung ziemlich aufregend ist: Sie schreiben ein paar Zeilen Code und erhalten viel mehr davon als Gegenleistung, die Sie möglicherweise manuell schreiben müssten. So fällt es leicht, damit in eine Einheitsfalle zu geraten:
"Wenn das einzige Werkzeug, das Sie haben, ein Hammer ist, neigen Sie dazu, jedes Problem als Nagel zu betrachten" - A. Maslow
Die Codegenerierung ist jedoch fast immer eine schlechte Idee. Ich verweise Sie auf diesen Beitrag, in dem die meisten Probleme bei der Codegenerierung erläutert werden. Kurz gesagt, die Codegenerierung führt zu unflexiblem und schwer zu pflegendem Code.
Hier sind einige Beispiele, wo Sie die Codegenerierung nicht verwenden sollten:
- Mit der Code-generierten verteilten Architektur führen Sie ein Skript aus, das die Serviceverträge und Implementierungen generiert und Ihre Anwendung auf magische Weise in eine verteilte Architektur verwandelt. Dies erkennt offensichtlich nicht die übermäßige Chattiness von In-Process-Anrufen an, die sich über das Netzwerk dramatisch verlangsamt, und die Notwendigkeit einer ordnungsgemäßen Ausnahme- und Transaktionsbehandlung verteilter Systeme usw.
- Visual GUI-Designer werden von Microsoft-Entwicklern seit langem verwendet (in Windows / Web Forms und teilweise in XAML-basierten Anwendungen), wo sie Widgets und UI-Elemente ziehen und ablegen und den für sie generierten (hässlichen) UI-Code hinter den Kulissen sehen.
- Naked Objects ist ein Ansatz für die Softwareentwicklung, bei dem Sie Ihr Domänenmodell definieren und der Rest Ihrer Anwendung, einschließlich der Benutzeroberfläche und der Datenbank, für Sie generiert wird. Konzeptionell ist es der modellgetriebenen Architektur sehr nahe.
- Model Driven Architecture ist ein Ansatz für die Softwareentwicklung, bei dem Sie Ihre Domäne mithilfe eines PIM (Platform Independence Model) detailliert angeben. Mithilfe der Codegenerierung wird PIM später in ein plattformspezifisches Modell (PSM) umgewandelt, das von einem Computer ausgeführt werden kann. Eines der Hauptverkaufsargumente von MDA ist, dass Sie das PIM einmal angeben und Web- oder Desktopanwendungen in einer Vielzahl von Programmiersprachen generieren können, indem Sie einfach eine Taste drücken, mit der der gewünschte PSM-Code generiert werden kann.
Basierend auf dieser Idee werden viele RAD-Tools (Rapid Application Development) erstellt: Sie zeichnen ein Modell und klicken auf eine Schaltfläche, um eine vollständige Anwendung zu erhalten. Einige dieser Tools gehen sogar so weit, Entwickler vollständig aus der Gleichung zu entfernen, in der nicht-technische Benutzer in der Lage sein sollen, sichere Änderungen an der Software vorzunehmen, ohne dass ein Entwickler erforderlich ist.
Ich wollte auch Object Relational Mapping in die Liste aufnehmen, da einige ORMs stark auf die Codegenerierung angewiesen sind, um das Persistenzmodell aus einem konzeptionellen oder physischen Datenmodell zu erstellen. Ich habe einige dieser Tools verwendet und einige Probleme beim Anpassen des generierten Codes gehabt. Trotzdem mögen viele Entwickler sie wirklich, also habe ich das einfach weggelassen (oder habe ich?!) ;)
Während einige dieser "Tools" einige der Programmierprobleme lösen und den erforderlichen Aufwand und die Kosten für die Softwareentwicklung im Voraus reduzieren, entstehen bei der Verwendung der Codegenerierung enorme versteckte Wartbarkeitskosten, die Sie früher oder später und umso mehr stören werden Generierter Code, den Sie haben, desto mehr wird es weh tun.
Ich weiß, dass viele Entwickler große Fans der Codegenerierung sind und jeden Tag ein neues Codegenerierungsskript schreiben. Wenn Sie in diesem Lager sind und denken, dass es ein großartiges Werkzeug für viele Probleme ist, werde ich nicht mit Ihnen streiten. Schließlich geht es in diesem Beitrag nicht darum zu beweisen, dass die Codegenerierung eine schlechte Idee ist.
Manchmal, nur manchmal, könnte die Codegenerierung eine gute Idee sein
Sehr selten befinde ich mich jedoch in einer Situation, in der die Codegenerierung gut zu dem vorliegenden Problem passt und die alternativen Lösungen entweder schwieriger oder hässlicher wären.
Hier sind einige Beispiele, wo die Codegenerierung gut passt:
- Sie müssen viel Boilerplate-Code schreiben, der einem ähnlichen statischen Muster folgt. Bevor Sie versuchen, Code zu generieren, sollten Sie in diesem Fall über das Problem nachdenken und versuchen, diesen Code richtig zu schreiben (z. B. mit objektorientierten Mustern, wenn Sie OO-Code schreiben). Wenn Sie sich bemüht haben und keine gute Lösung gefunden haben, ist die Codegenerierung möglicherweise eine gute Wahl.
- Sie verwenden sehr häufig statische Metadaten aus einer Ressource, und das Abrufen der Daten erfordert die Verwendung magischer Zeichenfolgen (und ist möglicherweise eine kostspielige Operation). Hier einige Beispiele:
- Durch Reflektion abgerufene Code-Metadaten: Das Aufrufen von Code mithilfe von Reflection erfordert magische Zeichenfolgen. Zur Entwurfszeit wissen Sie jedoch, was Sie benötigen. Mithilfe der Codegenerierung können Sie die erforderlichen Artefakte generieren. Auf diese Weise vermeiden Sie die Verwendung von Reflexionen zur Laufzeit und / oder magischen Zeichenfolgen in Ihrem Code. Ein gutes Beispiel für dieses Konzept ist T4MVC, das stark typisierte Helfer erstellt, die an vielen Stellen die Verwendung von Literalzeichenfolgen eliminieren.
- Statische Such-Webdienste: Hin und wieder stoße ich auf Webdienste, die nur statische Daten bereitstellen, die durch Bereitstellung eines Schlüssels abgerufen werden können, der als magische Zeichenfolge in der Codebasis endet. Wenn Sie in diesem Fall alle Schlüssel programmgesteuert abrufen können, können Sie durch Code eine statische Klasse generieren, die alle Schlüssel enthält, und auf die Zeichenfolgenwerte als stark typisierte Bürger erster Klasse in Ihrer Codebasis zugreifen, anstatt magische Zeichenfolgen zu verwenden. Sie können die Klasse natürlich manuell erstellen. Sie müssten es aber auch jedes Mal manuell warten, wenn sich die Daten ändern. Sie können diese Klasse dann verwenden, um den Webdienst aufzurufen und das Ergebnis zwischenzuspeichern, damit die nachfolgenden Aufrufe aus dem Speicher aufgelöst werden.
Alternativ können Sie, falls zulässig, einfach den gesamten Dienst im Code generieren, sodass der Suchdienst zur Laufzeit nicht erforderlich ist. Beide Lösungen haben einige Vor- und Nachteile. Wählen Sie also diejenige aus, die Ihren Anforderungen entspricht. Letzteres ist nur nützlich, wenn die Schlüssel nur von der Anwendung verwendet und nicht vom Benutzer bereitgestellt werden. Andernfalls werden früher oder später die Servicedaten aktualisiert, aber Sie haben den Code nicht generiert, und die vom Benutzer initiierte Suche schlägt fehl.
- Statische Nachschlagetabellen: Dies ist statischen Webdiensten sehr ähnlich, aber die Daten befinden sich in einem Datenspeicher im Gegensatz zu einem Webdienst.
Wie oben erwähnt, macht die Codegenerierung den Code unflexibel und schwer zu pflegen. Wenn die Art des Problems, das Sie lösen, statisch ist und keine häufige Wartung erfordert, ist die Codegenerierung möglicherweise eine gute Lösung!
Nur weil Ihr Problem in eine der oben genannten Kategorien passt, bedeutet dies nicht, dass die Codegenerierung gut dazu passt. Sie sollten dennoch versuchen, alternative Lösungen zu bewerten und Ihre Optionen abzuwägen.
Wenn Sie sich für die Codegenerierung entscheiden, stellen Sie außerdem sicher, dass Sie weiterhin Komponententests schreiben. Aus irgendeinem Grund denken einige Entwickler, dass generierter Code keine Komponententests erfordert. Vielleicht denken sie, dass es von Computern erzeugt wird und Computer keine Fehler machen! Ich denke, generierter Code erfordert genauso viel (wenn nicht mehr) automatisierte Überprüfung. Ich persönlich TDD meine Codegenerierung: Ich schreibe zuerst die Tests, führe sie aus, um zu sehen, dass sie fehlschlagen, dann generiere ich den Code und sehe, wie die Tests bestanden werden.
Toolkit zur Transformation von Textvorlagen
In Visual Studio gibt es eine großartige Codegenerierungs-Engine namens Text Template Transformation Toolkit (AKA, T4).
Von MSDN:
Textvorlagen bestehen aus folgenden Teilen:
- Anweisungen: Elemente, die steuern, wie die Vorlage verarbeitet wird.
- Textblöcke: Inhalt, der direkt in die Ausgabe kopiert wird.
- Steuerblöcke: Programmcode, der variable Werte in den Text einfügt und bedingte oder wiederholte Teile des Textes steuert.
Anstatt darüber zu sprechen, wie T4 funktioniert, möchte ich ein echtes Beispiel verwenden. Hier ist also ein Problem, mit dem ich vor einiger Zeit konfrontiert war und für das ich T4 verwendet habe. Ich habe eine Open-Source-.NET-Bibliothek namens Humanizer. Eines der Dinge, die ich in Humanizer bereitstellen wollte, war eine flüssige entwicklerfreundliche API für die Arbeit mit DateTime
.
Ich habe einige Variationen der API in Betracht gezogen und mich am Ende damit zufrieden gegeben:
1 |
In.January // Returns 1st of January of the current year |
2 |
In.FebruaryOf(2009) // Returns 1st of February of 2009 |
3 |
|
4 |
On.January.The4th // Returns 4th of January of the current year |
5 |
On.February.The(12) // Returns 12th of Feb of the current year |
6 |
|
7 |
In.One.Second // DateTime.UtcNow.AddSeconds(1); |
8 |
In.Two.Minutes // With corresponding From method |
9 |
In.Three.Hours // With corresponding From method |
10 |
In.Five.Days // With corresponding From method |
11 |
In.Six.Weeks // With corresponding From method |
12 |
In.Seven.Months // With corresponding From method |
13 |
In.Eight.Years // With corresponding From method |
14 |
In.Two.SecondsFrom(DateTime dateTime) |
Nachdem ich wusste, wie meine API aussehen würde, dachte ich über ein paar verschiedene Möglichkeiten nach, um dies zu beheben, und gab ein paar objektorientierte Lösungen heraus, aber alle erforderten ein gutes Stück Boilerplate-Code und diejenigen, die dies nicht taten, wollten es nicht Gib mir die saubere öffentliche API, die ich wollte. Also habe ich mich für die Codegenerierung entschieden.
Für jede Variation habe ich eine separate T4-Datei erstellt:
- In.Months.tt für
In.January
undIn.FebrurayOf(<some year>)
und so weiter. - On.Days.tt für
On.January.The4th
,On.February.The(12)
und so weiter. - In.SomeTimeFrom.tt für
In.One.Second
,In.TwoSecondsFrom(<date time>)
,In.Three.Minutes
und so weiter.
Hier werde ich On.Days
diskutieren. Der Code wird hier als Referenz kopiert:
1 |
<#@ template debug="true" hostSpecific="true" #> |
2 |
<#@ output extension=".cs" #> |
3 |
<#@ Assembly Name="System.Core" #> |
4 |
<#@ Assembly Name="System.Windows.Forms" #> |
5 |
<#@ assembly name="$(SolutionDir)Humanizer\bin\Debug\Humanizer.dll" #> |
6 |
<#@ import namespace="System" #> |
7 |
<#@ import namespace="Humanizer" #> |
8 |
<#@ import namespace="System.IO" #> |
9 |
<#@ import namespace="System.Diagnostics" #> |
10 |
<#@ import namespace="System.Linq" #> |
11 |
<#@ import namespace="System.Collections" #> |
12 |
<#@ import namespace="System.Collections.Generic" #> |
13 |
using System; |
14 |
|
15 |
namespace Humanizer |
16 |
{
|
17 |
public partial class On |
18 |
{
|
19 |
<# |
20 |
const int leapYear = 2012; |
21 |
for (int month = 1; month <= 12; month++) |
22 |
{
|
23 |
var firstDayOfMonth = new DateTime(leapYear, month, 1); |
24 |
var monthName = firstDayOfMonth.ToString("MMMM");#> |
25 |
|
26 |
/// <summary>
|
27 |
/// Provides fluent date accessors for <#= monthName #>
|
28 |
/// </summary>
|
29 |
public class <#= monthName #> |
30 |
{
|
31 |
/// <summary>
|
32 |
/// The nth day of <#= monthName #> of the current year
|
33 |
/// </summary>
|
34 |
public static DateTime The(int dayNumber) |
35 |
{
|
36 |
return new DateTime(DateTime.Now.Year, <#= month #>, dayNumber); |
37 |
}
|
38 |
<#for (int day = 1; day <= DateTime.DaysInMonth(leapYear, month); day++) |
39 |
{
|
40 |
var ordinalDay = day.Ordinalize();#> |
41 |
|
42 |
/// <summary>
|
43 |
/// The <#= ordinalDay #> day of <#= monthName #> of the current year
|
44 |
/// </summary>
|
45 |
public static DateTime The<#= ordinalDay #> |
46 |
{
|
47 |
get { return new DateTime(DateTime.Now.Year, <#= month #>, <#= day #>); } |
48 |
}
|
49 |
<#}#> |
50 |
}
|
51 |
<#}#> |
52 |
}
|
53 |
}
|
Wenn Sie diesen Code in Visual Studio auschecken oder mit T4 arbeiten möchten, stellen Sie sicher, dass Sie den Tangible T4-Editor für Visual Studio installiert haben. Es bietet IntelliSense, T4-Syntax-Highlighting, Advanced T4 Debugger und T4 Transform on Build.
Der Code mag am Anfang etwas beängstigend erscheinen, aber es ist nur ein Skript, das der ASP-Sprache sehr ähnlich ist. Beim Speichern wird eine Klasse namens On
mit 12 Unterklassen generiert, eine pro Monat (z. B. Januar
, Februar
usw.) mit jeweils öffentlichen statischen Eigenschaften, die einen bestimmten Tag in diesem Monat zurückgeben. Lassen Sie uns den Code auseinander brechen und sehen, wie es funktioniert.
Richtlinien
Die Syntax von Direktiven lautet: <#@ DirectiveName[AttributeName = "AttributeValue"] ... #>
. Weitere Informationen zu Richtlinien finden Sie hier.
Ich habe die folgenden Anweisungen im Code verwendet:
Vorlage
1 |
<#@ template debug="true" hostSpecific="true" #> |
Die Template-Direktive verfügt über mehrere Attribute, mit denen Sie verschiedene Aspekte der Transformation angeben können.
Wenn das debug
-Attribut true
ist, enthält die Zwischencodedatei Informationen, mit denen der Debugger die Position in Ihrer Vorlage, an der eine Unterbrechung oder Ausnahme aufgetreten ist, genauer identifizieren kann. Ich lasse das immer als true
.
Ausgabe
1 |
<#@ output extension=".cs" #> |
Die Output-Direktive wird verwendet, um die Dateinamenerweiterung und die Codierung der transformierten Datei zu definieren. Hier setzen wir die Erweiterung auf .cs
, was bedeutet, dass die generierte Datei in C# und der Dateiname On.Days.cs
lautet.
Versammlung
1 |
<#@ assembly Name="System.Core" #> |
Hier laden wir System.Core
, damit wir es in den Codeblöcken weiter unten verwenden können.
Die Assembly-Direktive lädt eine Assembly, damit Ihr Vorlagencode ihre Typen verwenden kann. Der Effekt ähnelt dem Hinzufügen einer Assemblyreferenz in einem Visual Studio-Projekt.
Dies bedeutet, dass Sie das .NET Framework in Ihrer T4-Vorlage voll ausnutzen können. Sie können beispielsweise ADO.NET verwenden, um eine Datenbank aufzurufen, einige Daten aus einer Tabelle zu lesen und diese für die Codegenerierung zu verwenden.
Weiter unten habe ich die folgende Zeile:
1 |
<#@ assembly name="$(SolutionDir)Humanizer\bin\Debug\Humanizer.dll" #> |
Das ist ein bisschen interessant. In der On.Days.tt
-Vorlage verwende ich die Ordinalize-Methode von Humanizer, die eine Zahl in eine Ordnungszeichenfolge umwandelt, um die Position in einer geordneten Reihenfolge wie der 1., der 2., der 3., der 4. zu bezeichnen. Dies wird verwendet, um The1st
, The2nd
usw. zu generieren.
Aus dem MSDN-Artikel:
Der Assemblyname sollte einer der folgenden sein:
- Der starke Name einer Assembly im GAC, z. B.
System.Xml.dll
. Sie können auch die Langform verwenden, z. B. name="System.Xml, Version=4.0.0.0, Culture = neutral, PublicKeyToken=b77a5c561934e089". Weitere Informationen finden Sie unterAssemblyName
. - Der absolute Weg der Montage.
System.Core
lebt in GAC, daher können wir den Namen einfach verwenden. aber für Humanizer müssen wir den absoluten Weg angeben. Natürlich möchte ich meinen lokalen Pfad nicht fest codieren, daher habe ich $(SolutionDir)
verwendet, das durch den Pfad ersetzt wird, in dem sich die Lösung während der Codegenerierung befindet. Auf diese Weise funktioniert die Codegenerierung für alle, unabhängig davon, wo sie den Code aufbewahren.
Importieren
1 |
<#@ import namespace="System" #> |
Mit der Importanweisung können Sie auf Elemente in einem anderen Namespace verweisen, ohne einen vollständig qualifizierten Namen anzugeben. Dies entspricht der using
-Anweisung in C# oder dem Import
in Visual Basic.
Oben definieren wir alle Namespaces, die wir in den Codeblöcken benötigen. Die dort angezeigten Import
blöcke werden meist von T4 Tangible eingefügt. Das einzige, was ich hinzugefügt habe, war:
1 |
<#@ import namespace="Humanizer" #> |
So kann ich später schreiben:
1 |
var ordinalDay = day.Ordinalize(); |
Ohne die import
-Anweisung und die Angabe der Assembly
nach Pfad anstelle einer C# -Datei hätte ich einen Kompilierungsfehler erhalten, der sich darüber beschwert, dass die Ordinalize
-Methode nicht für Ganzzahlen gefunden wurde.
Textblöcke
Ein Textblock fügt Text direkt in die Ausgabedatei ein. Oben habe ich einige Zeilen C# -Code geschrieben, die direkt in die generierte Datei kopiert werden:
1 |
using System; namespace Humanizer { public partial class On { |
Weiter unten, zwischen den Steuerblöcken, habe ich einige andere Textblöcke für die API-Dokumentation, Methoden und auch zum Schließen von Klammern.
Steuerblöcke
Steuerblöcke sind Abschnitte des Programmcodes, mit denen die Vorlagen transformiert werden. Die Standardsprache ist C#.
Hinweis: Die Sprache, in der Sie den Code in die Steuerblöcke schreiben, hängt nicht mit der Sprache des generierten Texts zusammen.
Es gibt drei verschiedene Arten von Steuerblöcken: Standard, Ausdruck und Klassenfunktion.
Von MSDN:
-
<# Standard control blocks #>
können Anweisungen enthalten. -
<#= Expression control blocks #>
können Ausdrücke enthalten. -
<#+ Class feature control blocks #>
können Methoden, Felder und Eigenschaften enthalten.
Werfen wir einen Blick auf die Steuerelemente, die wir in der Beispielvorlage haben:
1 |
<# |
2 |
const int leapYear = 2012; |
3 |
for (int month = 1; month <= 12; month++) |
4 |
{
|
5 |
var firstDayOfMonth = new DateTime(leapYear, month, 1); |
6 |
var monthName = firstDayOfMonth.ToString("MMMM");#> |
7 |
|
8 |
/// <summary>
|
9 |
/// Provides fluent date accessors for <#= monthName #>
|
10 |
/// </summary>
|
11 |
public class <#= monthName #> |
12 |
{
|
13 |
/// <summary>
|
14 |
/// The nth day of <#= monthName #> of the current year
|
15 |
/// </summary>
|
16 |
public static DateTime The(int dayNumber) |
17 |
{
|
18 |
return new DateTime(DateTime.Now.Year, <#= month #>, dayNumber); |
19 |
}
|
20 |
<#for (int day = 1; day <= DateTime.DaysInMonth(leapYear, month); day++) |
21 |
{
|
22 |
var ordinalDay = day.Ordinalize();#> |
23 |
|
24 |
/// <summary>
|
25 |
/// The <#= ordinalDay #> day of <#= monthName #> of the current year
|
26 |
/// </summary>
|
27 |
public static DateTime The<#= ordinalDay #> |
28 |
{
|
29 |
get { return new DateTime(DateTime.Now.Year, <#= month #>, <#= day #>); } |
30 |
}
|
31 |
<#}#> |
32 |
}
|
33 |
<#}#> |
Für mich persönlich ist das Verwirrendste an T4 das Öffnen und Schließen von Steuerblöcken, da sie sich mit den Klammern im Textblock vermischen (wenn Sie Code für eine Sprache mit geschweiften Klammern wie C# generieren). Ich finde, der einfachste Weg, damit umzugehen, besteht darin, den Steuerblock zu schließen(#>
), sobald ich ihn öffne(<#
) und dann den Code hinein zu schreiben.
Oben im Standard-Steuerblock definiere ich leapYear
als konstanten Wert. Auf diese Weise kann ich einen Eintrag für den 29. Februar erstellen. Dann iteriere ich über 12 Monate für jeden Monat und erhalte den firstDayOfMonth
und den monthName
. Ich schließe dann den Steuerblock, um einen Textblock für die Monatsklasse und ihre XML-Dokumentation zu schreiben. Der monthName
wird als Klassenname und in XML-Kommentaren verwendet (unter Verwendung von Ausdruckssteuerblöcken). Der Rest ist nur normaler C# -Code, mit dem ich Sie nicht langweilen werde.
Abschluss
In diesem Beitrag habe ich über die Codegenerierung gesprochen, einige Beispiele dafür gegeben, wann die Codegenerierung entweder gefährlich oder nützlich sein kann, und anhand eines realen Beispiels gezeigt, wie Sie mithilfe von T4-Vorlagen Code aus Visual Studio generieren können.
Wenn Sie mehr über T4 erfahren möchten, finden Sie auf Oleg Sychs Blog viele großartige Inhalte.