Россия, Рубцовск |
Техники генерации кода
Шаблоны XSLT
Так как генерация кода в какой-то мере является преобразованием текста, то можно использовать технологии работы с текстом, например XML. Применяя технологию XSLT можно преобразовать метаданные в программный код.
Рассмотрим пример, в котором требуется создать несколько объявлений констант, опишем их названия и значения в следующем XML-файле:
<?xml-stylesheet type="text/xsl" href="variables.xsl"?> <VARIABLES> <VARIABLE Active="true"> <VAR>A</VAR> <VAL>1</VAL> </VARIABLE> <VARIABLE Active="true"> <VAR>B</VAR> <VAL>2</VAL> </VARIABLE> <VARIABLE Active="true"> <VAR>C</VAR> <VAL>3</VAL> </VARIABLE> <VARIABLE Active="false"> <VAR>D</VAR> <VAL>4</VAL> </VARIABLE> </VARIABLES>Пример 2.8. Файл variables.xml
Обратим внимание на константу D. Свойство Active установлено в значение false. У других же констант установлено значение true. Это означает, что для константы D код генерироваться не будет. Также обратим внимание, что в первой строке указан файл стиля variables.xsl. В этом файле будут указаны правила преобразования XML-файла.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/TR/WD-xsl"> <xsl:template match="/"> <xsl:for-each select="VARIABLES/VARIABLE[@Active='true']"> const int <xsl:value-of select="VAR"/> = <xsl:value-of select="VAL"/>;<BR/> </xsl:for-each> </xsl:template> </xsl:stylesheet>Пример 2.9. Файл variables.xsl
В файле стилей указано, что для каждого элемента VARIABLE, имеющего свойство Active со значением true, будут выполняться следующие действия. Выводится строка, содержащая в самом начале текст "const int", потом имя константы (содержимое элемента VAR), затем знак равенства, после него отображается значение константы (содержимое элемента VAL). В конце стоят точка с запятой и тэг перехода на следующую строку.
const int A = 1; const int B = 2; const int C = 3;
Более подробно технология XSLT будет рассмотрена в "Применение языка преобразований XSLT" .
Шаблоны T4
Кроме XSLT существуют и другие технологии для работы с шаблонами. Одна из них - это технология T4. В среде Visual Studio имеется возможность применять шаблоны T4. Для этого необходимо создать файл с расширением ".tt", после чего добавить код на одном из языков Visual Basic или C#, например, следующий:
<#@ template debug="false" language="C#" #> <#@ output extension=".cs" #> <# string[] vars = new string [] {"A", "B", "C"}; #> class MyClass { <# foreach (string variable in vars) { #> private int <#= variable #> = 0; <# } #> }Пример 2.10.
Этот файл напоминает стили PHP или ASP.Net. Простым текстом написаны повторяющиеся статические участки кода, а между символами <# и #> расположен обрабатывающий программный код. Более подробно генерация кода при помощи шаблонов T4 будет рассмотрена в "Технология текстовых шаблонов T4" . Результатом генерации по указанному выше шаблону будет следующий код:
class MyClass { private int A = 0; private int B = 0; private int C = 0; }
Применение регулярных выражений
Трудно переоценить роль регулярных выражений в генерации кода. Этот инструмент манипуляции текстом позволяет облегчить многие задачи, так как генераторы кода много работают с текстом. Регулярные выражения являются шаблонами для выполнения поиска подстроки в тексте или замены этой подстроки на другую подстроку. Значительно облегчается чтение из конфигурационных файлов, поиск и замена исходного кода в файлах.
Рассмотрим пример, когда согласно приведенным комментариям необходимо добавить объявление параметров.
List<string> program = new List<string>(); string line; Regex pattern = new Regex(@"^//\s*(?<val>.*?)\s*=", RegexOptions.Singleline); using (StreamReader sr = File.OpenText(filepath)) { while (!sr.EndOfStream) { line = sr.ReadLine(); foreach (Match m in pattern.Matches(line)) { program.Add("private int " + m.Groups["val"].Value + ";"); } program.Add(Regex.Replace(line,@"^//\s*","")); } } Output.PutResult(program, filepath);Пример 2.11.
Здесь построчно считывается файл, для каждой строки проверяется соответствие шаблону "^//\s*(?<val>.*?)\s*=". В этом шаблоне ищутся подстроки, которые соответствуют следующему формату: начинаются знаками комментариев "//" в начале строки, затем идут ноль или больше пробелов, после чего могут встречаться любые символы (в скобках сохраняются в <val>), затем снова ноль или больше пробелов и знак равенства.
Для файла со следующими комментариями:
// a = 1; // b = 2; // c = a + b;
будет выведен в файл следующий код.
private int a; a = 1; private int b; b = 2; private int c; c = a + b;
Генерация незавершенного кода
Не во всех случаях требуется или есть возможность сгенерировать завершенный код, который не нуждается в ручной модификации после генерации. Иногда более выгодно для экономии времени и ресурсов быстро создать генератор, выводящий большой объем неполного кода. Затем вручную изменить части кода, трудно поддающиеся генерации.
Генерация незавершенного кода чаще всего выгодна, когда требуется разработать код всего один раз, сам код достаточно легко поддается генерации, за исключением некоторых небольших его частей, реализация которых сильно усложняет генератор. Если же код требуется пересоздавать много раз, то частая ручная модификация будет неприемлемым решением.
Рассмотрим случай генерации условия вида if-then-else. Условия внутри if могут задаваться в любом количестве. Последовательность их применения также может быть любой. Следовательно, скобки тоже могут быть любой сложности и любого уровня вложенности. Реализация такой структуры и ввод данных будет трудоемким занятием. Когда код требуется сгенерировать только один раз, то проще выполнить неполную генерацию. Что мы и сделаем. В XML-файле объявим секции ifsection, thensection, elsesection. В ifsection находятся логические условия, однако не будут указаны скобки и операции и\или, а в thensection и elsesection - операторы, которые надо выполнить в зависимости от истинности или ложности условия.
<?xml version="1.0" encoding="utf-8" ?> <condition> <ifsection> <ifcase>y==1</ifcase> <ifcase>k!=0</ifcase> <ifcase>SomeFunction(z)</ifcase> </ifsection> <thensection> <action>t = y + k</action> <action>SomeProcedure(t,z)</action> </thensection> <elsesection> <action>z++</action> </elsesection> </condition>Пример 2.12.
Рассмотрим программу, выполняющую чтение файла и генерацию кода.
string xmlpath = @"A:\input\ifcondition.xml"; string programpath = @"A:\input\ifcondition.cs"; List<string> program = new List<string>(); List<string> thensection = new List<string>(); List<string> elsesection = new List<string>(); string ifsection = "if ("; thensection.Add("{"); elsesection.Add("else {"); XmlDocument reader = new XmlDocument(); reader.Load(xmlpath); XmlElement elem = reader.DocumentElement; foreach(XmlElement child in elem.ChildNodes){ if (child.Name == "ifsection") { foreach (XmlElement ifcase in child.ChildNodes) { ifsection += "(" + ifcase.InnerText + ")???"; } } if (child.Name == "thensection") { foreach (XmlElement thencase in child.ChildNodes) { thensection.Add("\t" + thencase.InnerText + ";"); } } if (child.Name == "elsesection") { foreach (XmlElement elsecase in child.ChildNodes) { elsesection.Add("\t" + elsecase.InnerText + ";"); } } } ifsection += ")"; thensection.Add("}"); elsesection.Add("}"); program.Add(ifsection); program.AddRange(thensection); program.AddRange(elsesection); Output.PutResult(program, programpath);Пример 2.13.
В переменной xmlpath содержится путь к XML-файлу, а в переменной programpath путь к файлу сгенерированного кода. Программа в цикле пробегает по каждой из секций ifsection, thensection, elsesection. В секциях просматривается каждое условие или команда. Условия программа заключает в скобки и добавляет три вопросительных знака. Команды завершает точкой с запятой. Заметим, что для отступов используется символ табуляции "\t". Посмотрим на сгенерированный код:
if ((y==1)???(k!=0)???(SomeFunction(z))???) { t = y + k; SomeProcedure(t,z); } else { z++; }
Для завершения кода вручную надо поставить вместо вопросительных знаков логические операнды и добавить скобки в тех местах, где необходимо.
Шаги построения генератора
Рассмотрим схематически основные шаги в разработке приложения без применения генератора.
В самом начале выполняется проектирование архитектуры приложения, выявляются ее составные части и их взаимодействие. Когда архитектура становится ясной, выполняется разработка. При ее полном или частичном завершении выполняется тестирование разработанного кода. При необходимости по результатам тестирования выполняется отладка. Это только обобщенная схема, отражающая основные шаги. Такие этапы, как анализ предметной области, изначальный сбор требований, развертывание ПО и другие, не рассматриваются. Разумеется, реальный процесс разработки приложения будет содержать больше нюансов и деталей, которые могут варьироваться от проекта к проекту.
На любом из этапов жизни проекта, вплоть до эксплуатации, требования, предъявляемые программе, могут и будут изменяться. При изменении требований вносятся исправления в проект и выполняются разработка, тестирование и отладка измененных частей приложения. Таких случаев, что программа создана, активно используется и не требует никаких изменений и дополнений, практически не бывает.
А теперь посмотрим на такой же обобщенный процесс, но с применением генератора:
На этой схеме также рассматриваются основные шаги, имеющие отношение к разработке и сопровождению генератора кода. На каждый из шагов 2, 3, 4 добавляется похожий шаг, но связанный с применением генератора. Шаги, связанные с разработкой ручного кода остаются. На разных проектах доля ручного и сгенерированного кода может быть разной.
Разработка ручного кода дополняется генерацией кода. Ей предшествуют проектирование и разработка генераторов, создание шаблонов, выделение метаданных. Кроме тестирования ручного кода выполняется тестирование генератора и сгенерированного кода. А кроме отладки ручного кода может потребоваться отладка генератора и исправление шаблонов (но не отладка сгенерированного кода).
Формирование кода проекта по отношению к предметной области с учетом и генерации и ручной разработки будет выглядеть так:
В некоторых случаях генерацию нужно выполнять только для части метаданных. Для этого надо в метаданные добавить логический параметр, принимающий значения true и false и указывающий, нужно ли выполнять генерацию для этой части метаданных. Например, применять параметр to_generate или is_active. В примере 2.8 параметр назывался Active.