Статьи

Генерация кода с использованием T4

Мне не нравится генерация кода, и обычно я вижу это как «запах». Если вы используете генерацию кода любого рода, есть большая вероятность, что что-то не так с вашим дизайном или решением! Поэтому, возможно, вместо написания скрипта для генерации тысяч строк кода, вам следует сделать шаг назад, снова подумать о своей проблеме и найти лучшее решение. При этом существуют ситуации, когда генерация кода может быть хорошим решением.

В этой статье я расскажу о плюсах и минусах генерации кода, а затем покажу вам, как на примере использовать шаблоны 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 , 4 On.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 и указания 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, вы можете найти много отличного контента в блоге Олега Сыча .