Мне не нравится генерация кода, и обычно я вижу это как «запах». Если вы используете генерацию кода любого рода, есть большая вероятность, что что-то не так с вашим дизайном или решением! Поэтому, возможно, вместо написания скрипта для генерации тысяч строк кода, вам следует сделать шаг назад, снова подумать о своей проблеме и найти лучшее решение. При этом существуют ситуации, когда генерация кода может быть хорошим решением.
В этой статье я расскажу о плюсах и минусах генерации кода, а затем покажу вам, как на примере использовать шаблоны T4 , встроенный инструмент генерации кода в Visual Studio.
Генерация кода — плохая идея
Я пишу пост о концепции, которая, на мой взгляд, является плохой идеей, чаще всего нет, и было бы непрофессионально с моей стороны, если бы я вручил вам инструмент и не предупредил вас о его опасностях.
Правда в том, что генерация кода довольно увлекательна: вы пишете несколько строк кода и получаете намного больше взамен, что вам, возможно, придется писать вручную. Так что с ним легко попасть в ловушку, подходящую для всех:
« Если единственный инструмент, который у вас есть, это молоток, вы склонны рассматривать каждую проблему как гвоздь » ». А. Маслоу
Но генерация кода почти всегда плохая идея. Я отсылаю вас к этому посту , который объясняет большинство проблем, которые я вижу с генерацией кода. Короче говоря, генерация кода приводит к негибкости и сложности в обслуживании кода.
Вот несколько примеров, где вы не должны использовать генерацию кода:
- С распределенной архитектурой, генерируемой кодом, вы запускаете скрипт, который генерирует сервисные контракты и реализации и волшебным образом превращает ваше приложение в распределенную архитектуру. Это, очевидно, не в состоянии признать чрезмерную болтливость внутрипроцессных вызовов, которая резко замедляется по сети, и необходимость надлежащей обработки исключений и транзакций в распределенных системах и так далее.
- Визуальные дизайнеры GUI — это то, что разработчики Microsoft использовали целую вечность (в Windows / Web Forms и, в некоторой степени, в приложениях на основе XAML), где они перетаскивают виджеты и элементы пользовательского интерфейса и видят (уродливый) код пользовательского интерфейса, сгенерированный для них, за кулисами.
- «Голые объекты» — это подход к разработке программного обеспечения, при котором вы определяете модель своего домена, а все остальное в вашем приложении, включая пользовательский интерфейс и базу данных, генерируется для вас. Концептуально это очень близко к архитектуре, управляемой моделями.
- Модель на основе архитектуры — это подход к разработке программного обеспечения, при котором вы подробно указываете свой домен, используя модель независимости платформы (PIM). С помощью генерации кода PIM позже превращается в платформенно-ориентированную модель (PSM), которую может запустить компьютер. Одним из основных преимуществ MDA является то, что вы указываете PIM один раз и можете создавать веб-приложения или приложения для настольных компьютеров на различных языках программирования, просто нажимая кнопку, которая может генерировать нужный код PSM.
Многие инструменты RAD (Rapid Application Development) создаются на основе этой идеи: вы рисуете модель и нажимаете кнопку, чтобы получить законченное приложение. Некоторые из этих инструментов доходят до того, что пытаются полностью исключить разработчиков из уравнения, в котором, как считается, нетехнические пользователи могут вносить безопасные изменения в программное обеспечение без необходимости разработчика.
Я также собирался поместить объектно-реляционное отображение в список, поскольку некоторые ORM в значительной степени полагаются на генерацию кода для создания модели постоянства из концептуальной или физической модели данных. Я использовал некоторые из этих инструментов и испытал немалую боль при настройке сгенерированного кода. С учетом вышесказанного многим разработчикам они действительно нравятся, поэтому я просто не учел это (или я ?!);)
Хотя некоторые из этих «инструментов» действительно решают некоторые проблемы программирования и сокращают необходимые предварительные усилия и затраты на разработку программного обеспечения, при использовании генерации кода возникают огромные скрытые затраты на сопровождение, которые рано или поздно будут вас кусать, а тем более сгенерированный код у вас есть, тем более что это повредит.
Я знаю, что многие разработчики являются большими поклонниками генерации кода и пишут новый скрипт генерации кода каждый день. Если вы находитесь в этом лагере и думаете, что это отличный инструмент для решения множества проблем, я не буду с вами спорить. В конце концов, этот пост не о том, что генерация кода — плохая идея.
Иногда, только иногда, генерация кода может быть хорошей идеей
Однако очень редко я нахожусь в ситуации, когда генерация кода хорошо подходит для рассматриваемой проблемы, и альтернативные решения будут либо сложнее, либо ужаснее.
Вот несколько примеров, где генерация кода может быть подходящей:
- Вам нужно написать много шаблонного кода, который следует похожему статическому шаблону. Прежде чем пытаться сгенерировать код, в этом случае вам следует серьезно задуматься о проблеме и попытаться правильно написать этот код (например, используя объектно-ориентированные шаблоны, если вы пишете ОО-код). Если вы очень старались и не нашли хорошего решения, тогда генерация кода может быть хорошим выбором.
- Вы очень часто используете некоторые статические метаданные из ресурса, а для извлечения данных требуется использование волшебных строк (и, возможно, это дорогостоящая операция). Вот несколько примеров:
- Метаданные кода, полученные с помощью отражения : для вызова кода с использованием отражения требуются магические строки; но во время разработки вы знаете, что вам нужно, вы можете использовать генерацию кода для генерации требуемых артефактов. Таким образом вы избежите использования отражений во время выполнения и / или магических строк в вашем коде. Отличным примером этой концепции является T4MVC, который создает строго типизированные помощники, которые исключают использование литеральных строк во многих местах.
- Веб-сервисы со статическим поиском : время от времени я сталкиваюсь с веб-сервисами, которые предоставляют только статические данные, которые можно получить, предоставив ключ, который в конечном итоге становится магической строкой в базе кода. В этом случае, если вы можете программно получить все ключи, то вы можете создать статический класс, содержащий все ключи, и получить доступ к строковым значениям как строго типизированным гражданам первого класса в вашей кодовой базе вместо использования магических строк. Очевидно, вы можете создать класс вручную; но вам также придется поддерживать его вручную при каждом изменении данных. Затем вы можете использовать этот класс для обращения к веб-службе и кэширования результата, чтобы последующие вызовы разрешались из памяти.
В качестве альтернативы, если это разрешено, вы можете просто сгенерировать весь сервис в коде, чтобы служба поиска не требовалась во время выполнения. Оба решения имеют свои плюсы и минусы, поэтому выберите тот, который соответствует вашим требованиям. Последнее полезно только в том случае, если ключи используются только приложением и не предоставляются пользователем; в противном случае рано или поздно наступит момент, когда данные службы будут обновлены, но вы не сгенерировали код, и поиск, инициированный пользователем, завершится неудачно.
- Статические таблицы поиска : это очень похоже на статические веб-службы, но данные хранятся в хранилище данных, а не в веб-службе.
Как упоминалось выше, генерация кода делает его негибким и сложным в обслуживании; поэтому, если характер решаемой вами проблемы статичен и не требует частого обслуживания, генерация кода может быть хорошим решением!
То, что ваша проблема относится к одной из вышеперечисленных категорий, не означает, что генерация кода подходит для нее. Вы все равно должны попытаться оценить альтернативные решения и взвесить ваши варианты.
Кроме того, если вы идете для генерации кода, убедитесь, что все еще пишете модульные тесты. По какой-то причине некоторые разработчики считают, что сгенерированный код не требует модульного тестирования. Возможно, они думают, что это генерируется компьютерами, а компьютеры не делают ошибок! Я думаю, что сгенерированный код требует столько же (если не больше) автоматической проверки. Я лично использую TDD для генерации кода: сначала я пишу тесты, запускаю их, чтобы увидеть, как они проваливаются, затем генерирую код и вижу, что тесты пройдены.
Набор инструментов для преобразования текстовых шаблонов
В Visual Studio есть потрясающий механизм генерации кода, который называется Text Template Transformation Toolkit (AKA, T4).
Из MSDN :
Текстовые шаблоны состоят из следующих частей:
- Директивы : элементы, которые управляют обработкой шаблона.
- Текстовые блоки : содержимое, которое копируется непосредственно в вывод.
- Блоки управления : программный код, который вставляет значения переменных в текст и управляет условными или повторяющимися частями текста.
Вместо того, чтобы говорить о том, как работает Т4, я бы хотел привести реальный пример. Итак, вот проблема, с которой я столкнулся некоторое время назад, для которой я использовал T4. У меня есть библиотека с открытым исходным кодом .NET под названием Humanizer . Одна из вещей, которую я хотел предоставить в Humanizer, — это удобный API для работы с DateTime
.
Я рассмотрел довольно много вариантов API и в итоге остановился на этом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
In.January // Returns 1st of January of the current year
In.FebruaryOf(2009) // Returns 1st of February of 2009
On.January.The4th // Returns 4th of January of the current year
On.February.The(12) // Returns 12th of Feb of the current year
In.One.Second // DateTime.UtcNow.AddSeconds(1);
In.Two.Minutes // With corresponding From method
In.Three.Hours // With corresponding From method
In.Five.Days // With corresponding From method
In.Six.Weeks // With corresponding From method
In.Seven.Months // With corresponding From method
In.Eight.Years // With corresponding From method
In.Two.SecondsFrom(DateTime dateTime)
|
После того, как я узнал, как будет выглядеть мой API, я подумал о нескольких разных способах решения этой проблемы и добавил несколько объектно-ориентированных решений, но все они требовали довольно много стандартного кода, а те, которые этого не сделали, не будут дай мне чистый публичный API, который я хотел. Поэтому я решил пойти с генерацией кода.
Для каждого варианта я создал отдельный файл T4:
- In.Months.tt для
In.January
иIn.FebrurayOf(<some year>)
и так далее. - On.Days.tt для 1
On.January.The4th
, 4On.February.The(12)
февраля и т. Д. - In.SomeTimeFrom.tt для
In.One.Second
,In.TwoSecondsFrom(<date time>)
,In.Three.Minutes
и так далее.
Здесь я буду обсуждать On.Days
. Код скопирован здесь для вашей справки:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
<#@ 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» #>
using System;
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»);#>
/// <summary>
/// Provides fluent date accessors for <#= monthName #>
/// </summary>
public class <#= monthName #>
{
/// <summary>
/// The nth day of <#= monthName #> of the current year
/// </summary>
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();#>
/// <summary>
/// The <#= ordinalDay #> day of <#= monthName #> of the current year
/// </summary>
public static DateTime The<#= ordinalDay #>
{
get { return new DateTime(DateTime.Now.Year, <#= month #>, <#= day #>);
}
<#}#>
}
<#}#>
}
}
|
Если вы проверяете этот код в Visual Studio или хотите работать с T4, убедитесь, что вы установили Tangible T4 Editor для Visual Studio . Он предоставляет IntelliSense, подсветку синтаксиса T4, расширенный отладчик T4 и преобразование T4 при сборке.
Код может показаться немного пугающим в начале, но это просто скрипт, очень похожий на язык ASP. При сохранении будет сгенерирован класс с именем « On
12 подклассами, по одному в месяц (например, January
, February
т February
Д.), Каждый с общедоступными статическими свойствами, которые возвращают определенный день в этом месяце. Давайте разберем код на части и посмотрим, как он работает.
Директивы
Синтаксис директив следующий: <#@ DirectiveName [AttributeName = "AttributeValue"] ... #>
. Вы можете прочитать больше о директивах здесь .
Я использовал следующие директивы в коде:
шаблон
1
|
<#@ template debug=»true» hostSpecific=»true» #>
|
Директива Template имеет несколько атрибутов, которые позволяют вам указывать различные аспекты преобразования.
Если атрибут debug
имеет значение true
, файл промежуточного кода будет содержать информацию, которая позволяет отладчику более точно идентифицировать позицию в вашем шаблоне, где произошел разрыв или исключение. Я всегда оставляю это как true
.
Выход
1
|
<#@ output extension=».cs» #>
|
Директива Output используется для определения расширения имени файла и кодировки преобразованного файла. Здесь мы устанавливаем расширение .cs
что означает, что сгенерированный файл будет в C #, а имя файла будет On.Days.cs
сборочный
1
|
<#@ assembly Name=»System.Core» #>
|
Здесь мы загружаем System.Core
чтобы использовать его в блоках кода ниже.
Директива Assembly загружает сборку, чтобы код вашего шаблона мог использовать ее типы. Эффект похож на добавление ссылки на сборку в проекте Visual Studio.
Это означает, что вы можете в полной мере использовать .NET Framework в своем шаблоне T4. Например, вы можете использовать ADO.NET, чтобы получить доступ к базе данных, прочитать некоторые данные из таблицы и использовать их для генерации кода.
Далее у меня есть следующая строка:
1
|
<#@ assembly name=»$(SolutionDir)Humanizer\bin\Debug\Humanizer.dll» #>
|
Это немного интересно. В шаблоне On.Days.tt
я использую метод Ordinalize от Humanizer, который превращает число в порядковую строку, используемую для обозначения позиции в упорядоченной последовательности, такой как 1-й, 2-й, 3-й, 4-й. Это используется для генерации The1st
, The2nd
и так далее.
Из статьи MSDN :
Имя сборки должно быть одним из следующих:
- Строгое имя сборки в GAC, например
System.Xml.dll
. Вы также можете использовать длинную форму, например name = «System.Xml, Version = 4.0.0.0, Culture = нейтральный, PublicKeyToken = b77a5c561934e089». Для получения дополнительной информации см.AssemblyName
. - Абсолютный путь сборки.
System.Core
живет в GAC, поэтому мы можем легко использовать его имя; но для Humanizer мы должны предоставить абсолютный путь. Очевидно, я не хочу жестко кодировать мой локальный путь, поэтому я использовал $(SolutionDir)
который заменяется на путь, по которому живет решение во время генерации кода. Таким образом, генерация кода работает хорошо для всех, независимо от того, где они хранят код.
импорт
1
|
<#@ import namespace=»System» #>
|
Директива import позволяет ссылаться на элементы в другом пространстве имен без указания полностью определенного имени. Это эквивалент оператора using
в C # или imports
в Visual Basic.
Вверху мы определяем все пространства имен, которые нам нужны в блоках кода. Блоки import
вы видите там, в основном вставляются T4 Tangible. Единственное, что я добавил, было:
<#@ import namespace="Humanizer" #>
Поэтому я могу позже написать:
var ordinalDay = day.Ordinalize();
Без оператора import
и указания assembly
по пути вместо файла C # я получил бы ошибку компиляции с жалобой на отсутствие метода Ordinalize
для целого числа.
Текстовые блоки
Текстовый блок вставляет текст непосредственно в выходной файл. Вверху я написал несколько строк кода на C #, которые непосредственно копируются в сгенерированный файл:
1
|
using System;
|
Далее, между блоками управления, у меня есть несколько других текстовых блоков для документации API, методов, а также для закрытия скобок.
Блоки управления
Управляющие блоки — это разделы программного кода, которые используются для преобразования шаблонов. Язык по умолчанию — C #.
Примечание . Язык, на котором вы пишете код в блоках управления, не связан с языком генерируемого текста.
Существует три различных типа блоков управления: Стандарт, Выражение и Функция класса.
Из MSDN :
-
<# Standard control blocks #>
могут содержать операторы. -
<#= Expression control blocks #>
могут содержать выражения. -
<#+ Class feature control blocks #>
могут содержать методы, поля и свойства.
Давайте посмотрим на блоки управления, которые есть в образце шаблона:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
<#
const int leapYear = 2012;
for (int month = 1; month <= 12; month++)
{
var firstDayOfMonth = new DateTime(leapYear, month, 1);
var monthName = firstDayOfMonth.ToString(«MMMM»);#>
/// <summary>
/// Provides fluent date accessors for <#= monthName #>
/// </summary>
public class <#= monthName #>
{
/// <summary>
/// The nth day of <#= monthName #> of the current year
/// </summary>
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();#>
/// <summary>
/// The <#= ordinalDay #> day of <#= monthName #> of the current year
/// </summary>
public static DateTime The<#= ordinalDay #>
{
get { return new DateTime(DateTime.Now.Year, <#= month #>, <#= day #>);
}
<#}#>
}
<#}#>
|
Лично для меня самой запутанной вещью в T4 являются блоки управления открытием и закрытием, так как они как бы смешиваются со скобками в текстовом блоке (если вы генерируете код для языка фигурных скобок, такого как C #). Я считаю, что самый простой способ справиться с этим, это закрыть ( #>
) блок управления, как только я открою ( <#
) его, а затем написать код внутри.
Сверху, внутри стандартного блока управления, я определяю leapYear
как постоянное значение. Это так, я могу создать запись для 29 февраля. Затем я повторяю по 12 месяцев для каждого месяца, получая firstDayOfMonth
и monthName
. Затем я закрываю блок управления, чтобы написать текстовый блок для класса месяца и его документацию XML. monthName
используется как имя класса и в комментариях XML (с использованием блоков управления выражениями). Все остальное — обычный код C #, который я не собираюсь утомлять.
Вывод
В этой статье я говорил о генерации кода, предоставил несколько примеров, когда генерация кода может быть опасной или полезной, а также показал, как можно использовать шаблоны T4 для генерации кода из Visual Studio, используя реальный пример.
Если вы хотите узнать больше о T4, вы можете найти много отличного контента в блоге Олега Сыча .