Ориентация на язык

Автор: Дмитрий Кириллов
Опубликовано в журнале "Компьютерра" №10 от 16 марта 2006 года

Одну из самых актуальных, наболевших и, не побоюсь этого слова, фундаментальных проблем разработки можно кратко назвать так: "проект программы не равен ее исходному коду". Впечатляющий набор современных инструментов для анализа, проектирования, рефакторинга и реинжиниринга предназначен, по сути, для сокращения разрыва между проектом системы и ее реализацией в исходном коде.
Так как же сохранить качественный программный проект в условиях постоянно меняющихся требований заказчика? Статья, предлагаемая вашему вниманию, посвящена одному из потенциальных решений, которое способно существенно повлиять на сложившуюся практику разработки.

Об авторе

Лидер группы разработки в компании Nicotech International. В область профессиональных и академических интересов входят организация управления проектами, архитектура программного обеспечения, теория формальных языков, технологии порождающего программирования.

В качестве вступления следует отметить, что современные методы разработки программного обеспечения позволяют достаточно четко отделить бизнес-требования к системе от программной архитектуры, а уж тем более - от исходного кода реализации... но лишь на ранних стадиях разработки. При этом серьезное изменение проекта на поздних стадиях может стать тем самым "дятлом, залетевшим в форточку и разрушившим цивилизацию".

Для того чтобы этого не произошло, опытные разработчики и архитекторы рекомендуют:

  • пользуйтесь шаблонами проектирования (при этом снижаются риски, связанные с неудачным выбором архитектуры);
  • периодически проводите ревизии проекта (забавно, что при этом зачастую происходит документирование поведения системы "пост-фактум");
  • делайте архитектуру многослойной с минимальной зависимостью между слоями;
  • прототипируйте, выпуская сборки как можно чаще (золотое правило экстремального программирования);
  • определяйте возможные направления будущих изменений проекта (это уже из области технологий "третьего глаза").

    Этот список можно продолжать бесконечно, однако и так понятно, что подобные рекомендации позволяют лишь снизить риски, обусловленные расхождением проекта и исходного кода. Корень же всех зол кроется в том, что высокоуровневые аспекты проекта выражаются и документируются в терминах естественного языка (каким является, например, русский или английский), тогда как код реализации пишется на каком-нибудь формальном языке (C++, Java, C#). И между двумя этими типами языков лежит целая пропасть.

    Языки предметной области

    Решение проблемы напрашивается само собой: может, сразу излагать бизнес-требования на формальном языке? Или хотя бы не бизнес-требования (это мы сильно замахнулись), а высокоуровневые абстракции предметной области, из которых и состоит проект системы?

    Да вот только где бы найти подходящий язык! Очевидно, что универсальные языки программирования для этой цели непригодны: в описании функций системы никогда не встречаются термины наподобие "класс" или "виртуальный метод". Диаграммы UML тоже хороши только в качестве красивых иллюстраций к техническому проекту системы[Справедливости ради нужно отметить, что диаграммы классов и взаимодействия могут быть полезны и на этапе реализации, но они опять-таки не содержат "правильных" абстракций]. Еще несколько лет назад казалось, что с этой ролью справится XML, однако сейчас понятно, что подобные проблемы ему не по зубам (более подробно по этому поводу см. врезку "XML и XSLT").

    Вывод: подобные языки нужно создавать. Причем нужно создавать свой, особенный набор языков для каждого типа проектируемых систем, поскольку абстракции, на которых основана какая-нибудь бухгалтерская программа, сильно отличаются от абстракций системы по сбору данных для аналитической отчетности. Для таких языков даже существует устоявшийся термин - DSL (Domain-Specific Language, специализированный язык предметной области), - которым мы и будем пользоваться в дальнейшем.

    Идея языков предметной области стара как мир. Макросы, языки командных оболочек (shell-скрипты Unix, например), проблемно-ориентированные языки приложений (такие как встроенный язык известной в России системы "1С"), языки пользовательских интерфейсов (например, XUL, широко известный в сообществе Mozilla), даже "великий и могучий" SQL для работы с базами данных, - все вышеперечисленные языки относятся к категории DSL, поскольку каждый из них проектировался для своей предметной области. Вместе с тем, за редкими исключениями, DSL не используются в качестве средства разработки программных приложений. А ведь как было бы здорово: разработать DSL и записать на нем код проекта…

    "Создавать языки под каждый конкретный проект? Вздор!" - скажет вам любой специалист, хорошо знающий, как дорого обходится проектирование формальных языков с нуля. Действительно, при классической схеме разработки любого языка нужно написать много кода для распознавания исходных текстов на этом языке и их погружения в объектную модель, пригодную для дальнейших манипуляций. Кроме того, даже при условии, что эта работа проделана, возникает ряд вопросов.

    Как отображать и редактировать программы на таком языке? Понятно, что в век высоких скоростей и мощных сред разработки ограничиться простым текстовым редактором a-la "Блокнот" уже не получится: представления о производительности труда разработчика ныне совсем не те, что в "далекие" 90-е годы XX века.

    Какова стоимость внесения изменений в разработанный язык? Если для того, чтобы изменить какое-то понятие предметной области, нужно "перекроить" весь код распознавателя, выгода от использования такого подхода равна нулю.

    И главное: предположим, объектная модель получена. Что делать дальше? Ведь модель еще нужно связать с языком реализации системы, что является отдельной головной болью.

    Языковые инструментарии

    Перечисленные выше вопросы давно волнуют как сообщество разработчиков, так и компании, занимающиеся выпуском средств разработки, поскольку сама по себе идея DSL очень заманчива. Тем не менее по ряду причин комплексное решение вышеуказанных проблем могло появиться лишь совсем недавно.

    Речь идет о новом типе программного обеспечения - так называемых языковых инструментариях (language workbenches), которые являются полноценными средами разработки, специально заточенными под DSL. И хотя существуют пока лишь прототипы таких систем, совершенно непригодные для использования в реальных проектах, главные особенности языковых инструментариев можно наглядно продемонстрировать уже сейчас.

    Из разработок в этой области хотелось бы упомянуть два перспективных проекта. Первым из них является Meta Programming System компании JetBrains. Система MPS ориентирована на совместное использование с фирменной IDE компании - средой разработки Java-приложений IntelliJ IDEA, которой автор уже восхищался в статье "Кодируй да радуйся" ("КТ" #562). Другой проект, Software Factories, принадлежит перу софтверного гиганта Microsoft и выступает в качестве дополнения к недавно вышедшей Visual Studio 2005[Meta Programming System можно свободно скачать по адресу jetbrains.com/mps. Проект Software Factories в настоящий момент представляет собой набор инструментов DSL Tools, доступных для скачивания в составе SDK к Visual Studio 2005. Подробности можно узнать здесь: msdn.microsoft.com/vstudio/teamsystem/workshop/sf].

    Ну что ж, покончив с продолжительным введением, перейдем к самому интересному: технологии разработки программ в окружении языковых инструментариев.

    Что такое метапрограмма?

    Грубо говоря, метапрограмма — это программа, формирующая в результате своей работы другую программу. Сложно? Тем не менее все это интуитивно понятно человеку, хоть раз занимавшемуся разработкой динамических серверных страниц. Действительно, для веб-дизайнера HTML является «программой для браузера», а для веб-разработчика тот же HTML выступает в роли данных, которые нужно сформировать в результате работы некоторого серверного кода.

    Следует отметить важную особенность метапрограммирования: любая метапрограмма определяет не одну конкретную программу, а целый класс. Ниже приведен пример метапрограммы, которая определяет класс всех документов HTML, содержащих произвольное число параграфов. Текст метапрограммы (ASP.NET, С#) заключен между специальными операторами <%...%> и <%=...%>.
    <HTML>
    <BODY>
    <%foreach(Paragraph p in Document) {%>
    <p>
    <%=p.Content%>
    </p>
    <%}%>
    </BODY>
    </HTML>

     

    Проектирование DSL

    Создание языка предметной области начинается с этапа моделирования данных - той информации, которая будет впоследствии записана в терминах DSL. Для простоты предположим, что нам необходимо разработать DSL, на котором можно описать структуру статьи для журнала "Компьютерра". Предметная область этой задачи включает в себя понятия статья, раздел и подраздел[Понятие "подраздел", конечно же, является избыточным, но кому нужны эти скучные профессиональные детали?]. Для статьи характерны название, автор и некоторая структура, включающая в себя разделы статьи.

    Проиллюстрируем вышесказанное диаграммой (см. рис. 1), описывающей модель данных нашего DSL, который условно назовем "Структура статьи в КТ".

    Следующий этап проектирования состоит в том, чтобы внести разработанную нами модель данных в языковой инструментарий. При этом для каждого понятия предметной области необходимо создать соответствующую концепцию языка. Например, концепция "статья" выглядит в MPS так, как изображено на рис. 2. На этом проектирование нашего языка можно считать завершенным.

    Кто-нибудь может возразить: язык - это в первую очередь знаковая система, а то, что мы только что создали, является скорее некоторой объектной моделью. Действительно, мы разработали только часть языка, традиционно называемую абстрактным синтаксисом. Если так можно выразиться, "знаковой системой" DSL в контексте языковых инструментариев являются редакторы, обеспечивающие визуальное отображение понятий языка.

    Создание редактора

    Разработка редактора для DSL - занятие еще более увлекательное, нежели проектирование самого DSL, поскольку тут гораздо более широкое поле для творчества.

    MPS обладает встроенным дизайнером для создания редакторов DSL, основанном на идее вложенных ячеек. Поясним эту идею на примере редактора для концепции "статья". На рис. 3 ячейка-контейнер верхнего уровня содержит две дочерние ячейки, расположенные вертикально. Верхняя ячейка содержит константное слово "статья", а нижняя является горизонтальным контейнером для других ячеек. И так далее.

    После определения "раскладки" составных частей остается связать с редактором атрибуты "название" и "автор", дополнить его возможностью выбора автора из списка, и получится нечто, изображенное на рис. 4. Процесс редактирования документов при помощи такого редактора очень прост и вполне удобен (хотя и слегка непривычен). Например, для добавления подраздела необходимо перейти в ячейку "…добавьте подраздел…" и начать ввод текста. После нажатия клавиши Enter фокус ввода переместится на следующий подраздел.

    Генераторы синтаксических анализаторов

    К автоматизации процесса разработки DSL можно подходить с различных сторон. Классический путь, существовавший задолго до появления языковых инструментариев, заключается в создании грамматики DSL, пригодной для обработки специальными программами — генераторами синтаксических анализаторов.

    Генератор синтаксических анализаторов (ГСА) — это утилита, на вход которой поступает файл с описанием правил грамматики некоторого языка, называемого целевым. В результате работы генератор формирует исходные тексты на C++ (или, допустим, Java), содержащие код для обработки конструкций целевого языка и, возможно, для формирования объектной модели. Написание собственного ГСА «с изюминкой» долгое время являлось престижной академической работой в области computer science, поэтому число подобных инструментов сегодня исчисляется десятками. Этот факт даже получил отражение в названиях многих ГСА: «еще один компилятор компиляторов» (yacc), «еще один инструмент для распознавания языков» (ANTLR) и т. п.
    В качестве примера приведем фрагмент грамматики ANTLR для языка арифметических выражений, содержащих числа, а также операции ‘+’ и ‘*’. Хотя подобная запись и выглядит страшновато, при наличии определенных навыков она воспринимается достаточно легко.
    expr : mexpr (‘+’ mexpr)* ‘;’!;
    mexpr : number (‘*’ number)*;
    number : (‘0’..’9’)+;
    Несмотря на ряд трудностей, связанных с повсеместным применением ГСА, на сегодняшний день они являются распространенным средством автоматизации разбора исходных текстов*. Например, распознаватель SQL для широко известной открытой СУБД PostgreSQL разработан при помощи пары lex и yacc. Интересно отметить, что эта «сладкая парочка» оказала существенное влияние на открытый софт, породив целое направление так называемых «малых языков» (по сути своей являющихся DSL), с которыми пользователи *nix-систем часто имеют дело при редактировании конфигурационных файлов.


    * Тот, кто боролся с неоднозначностями и устранением левой рекурсии путем введения фиктивных правил в грамматику, хорошо понимает, трудности какого рода приходится преодолевать.

     

    Привязка DSL к языку реализации

    DSL сам по себе, пусть даже и с хорошим редактором, не представляет интереса до тех пор, пока мы не привяжем его понятия к языку реализации - как правило, некоторому универсальному языку программирования, например Java или С#. Для решения этой задачи в языковых инструментариях применяются технологии метапрограммирования (см. врезку "Что такое метапрограмма?").
    Вид метапрограммы существенно зависит как от структуры DSL, так и от языка реализации проекта. Например, в случае DSL "Структура статьи в КТ" можно сгенерировать документ HTML или макрос для Word, который в процессе выполнения сформирует шаблон будущей статьи с необходимой разметкой документа. При этом метапрограмма, генерирующая HTML, будет сильно отличаться от метапрограммы-генератора документа Word.

    Вообще говоря, метапрограммирование - интересная и мощная, но довольно сложная технология. Именно поэтому в окончательном варианте статьи опущен пример, связанный с написанием метапрограммы для нашего DSL "Структура статьи в КТ". Отметим лишь, что процесс написания метапрограмм можно радикально облегчить, если мы хорошо представляем себе конечный результат - исходный код на языке реализации. Поэтому при ведении проекта на DSL целесообразно использовать прототипирование, то есть вначале создать "скелет" разрабатываемого приложения, а уж затем проектировать DSL и метапрограммы-генераторы для него.

    Процесс разработки

    Обобщая, можно выделить следующие этапы разработки приложений с участием языковых инструментариев:

    1. Cоздание прототипа, содержащего частичную реализацию минимально необходимого набора бизнес-функций ("скелет" будущего приложения).
    2. Определение существенных абстракций проекта, разработка DSL.
    3. Создание метапрограмм-генераторов для понятий DSL.
    4. Перевод разработанного прототипа приложения в окружение языкового инструментария.
    5. Реализация бизнес-функций проекта в терминах DSL.
    6. Автоматическая генерация исходных текстов на языке реализации проекта.

    Этапы 2–6 схематически изображены на диаграмме (рис. 5), там же показана взаимосвязь между языковыми инструментариями и классическими средами разработки.

    Возможно, многим покажется, что вышеописанный подход к разработке - не более чем преодоление трудностей, которые мы сами же себе и создали, приняв решение вести проект системы на DSL, однако список полученных при этом преимуществ весьма внушителен:

  • полностью подавляется расхождение проекта и исходного кода. Высокоуровневые абстракции проекта, записанные на DSL, связаны с кодом реализации проекта. При этом проект всегда остается актуальным, ведь изменения исходного кода теперь вторичны по отношению к изменениям в самом DSL;
  • облегчаются доработки проекта под изменившиеся требования заказчика. Этому способствует сама философия метапрограммирования: DSL как бы параметризует проект. При небольших изменениях проекта разработчику нужно лишь слегка модифицировать конкретную конфигурацию, описанную на DSL. Серьезные изменения, затрагивающие предметную область, тоже становятся проще, поскольку в таких случаях алгоритм действий четко определен: вначале модифицируем сам DSL и редакторы для него, а затем - генераторы исходного кода на языке реализации;
  • упрощается управление проектом. Например, при вхождении в проект новых участников для получения общего представления о проекте (в его актуальном состоянии!) достаточно показать им лишь ту часть исходного кода, которая записана на DSL. Более того, взаимодействие между участниками проекта становится эффективнее. Допустим, аналитик проекта, хорошо представляющий себе бизнес-логику приложения, теперь сможет самостоятельно описывать на DSL ее отдельные части[В принципе, достаточно лишь понимания аналитиком кода на DSL. При этом он (как правило, все же, она) сможет на словах рассказать разработчикам, что нужно сделать, а затем, просмотрев код на DSL, проконтролировать их работу]. При этом проект избавляется от лишних документов, а разработчики - от их прочтения (на что тоже нужно время) и двусмысленного толкования;
  • становится возможным повторное использование DSL в схожих проектах. Все разработанные DSL, подобно общим библиотекам компонентов, становятся серьезным активом компании-разработчика, поскольку являются квинтэссенцией опыта компании и допускают повторное использование в будущих проектах.

    На мой взгляд, в настоящий момент у языковых инструментариев существует только один серьезный недостаток: риск попадания в зависимость от поставщика программного обеспечения. Дело в том, что сейчас DSL жестко привязан к языковому инструментарию, при помощи которого он был разработан. И если в мире Microsoft этот вопрос отпадает автоматически (ввиду тотального характера зависимости), то для будущих пользователей MPS он вполне актуален, даже несмотря на то, что все файлы проекта MPS хранятся в открытом формате XML. Разумеется, это не является непреодолимым препятствием на пути к использованию языковых инструментариев, тем более что стандартизация в этой области станет возможной лишь с появлением на рынке конкурирующих и несовместимых друг с другом продуктов. Просто к списку технологических рисков проекта добавляется еще один пунктик.

    Декларация или империя?

    Позволю себе внести предварительное замечание эстетического характера: в свете языковых инструментариев могут возродиться многие давно забытые дискуссии о самих языках программирования, придававшие нашему ремеслу (искусству?) особое, ни с чем не сравнимое обаяние и притягательную силу. Обаяние, которое автор статьи так настойчиво хотел похоронить, публично обличая страстных приверженцев C++, верных любимому языку по сей день[ «Язык мой...», «КТ» #555-556]. Каюсь, грешен: поддался влиянию поголовного обсишарпивания разработчиков. Прошли годы, и хотя в отношении самого C++ автор своих взглядов не изменил, за настойчивость в желании прервать диалог о языках программирования все же досадно.

    Итак, переходим к сути. Вообще говоря, при написании программ существуют две стратегии:

  • записать, что нам нужно сделать (декларативные языки);
  • записать, как мы собираемся это делать (императивные языки).

    Для декларативных языков характерны емкие и компактные конструкции. Достаточно сказать, что исходный текст довольно сложной программы на ПРОЛОГе — ярком представителе всего семейства — вполне может уместиться «на одном экране».

    Тем не менее практически весь мэйнстрим современной разработки составляют императивные языки (все те же С++, Java, C#, ...), а чуть ли не единственной серьезной промышленной нишей для декларативных языков являются технологии баз данных, где прочно обосновался SQL вкупе с множеством диалектов. И это не просто «исторически так сложилось». Несмотря на изящество, компактность и концептуальную простоту декларативных языков их применение оправданно только при условии, что предметная область задачи четко определена. Именно этим обстоятельством и объясняется успех SQL, ведь абстракции, на которых построена работа с реляционными данными, давно изучены и хорошо известны.

    При проектировании DSL актуален следующий вопрос: нужно ли вносить в него императивные конструкции (условные операторы и циклы)? Не потеряем ли мы в результате то, к чему, собственно, и стремимся: получение простого языка, на котором записана лишь самая суть проекта, без утомительных деталей реализации?

    Руководствуясь здравым смыслом, можно сказать, что императивные конструкции целесообразно использовать лишь тогда, когда это продиктовано спецификой предметной области задачи. Например, если в постановке задачи часто употребляется слово «коллекция элементов» и говорится о задачах добавления/удаления/поиска элементов, введение в DSL понятия «цикл для работы с коллекциями» способно существенно упростить решение. Тем не менее предпочтение все же стоит отдавать декларативным конструкциям: их проще моделировать (сокращаются затраты на проектирование DSL), к тому же зачастую они легче воспринимаются и несут больше информации о проекте.

     

    Прекрасное далёко

    Для создания DSL "Структура статьи в КТ" автору потребовалось около часа. Разумеется, это не есть адекватный показатель, поскольку мы разработали простой и вдобавок совершенно бесполезный язык, но трудозатраты в один человеко-час все-таки впечатляют. И тем не менее многие почему-то не верят, что разработка в окружении языковых инструментариев будет быстрой, а процесс создания и сопровождения DSL - простым. Мне кажется, здесь вполне уместна аналогия с рефакторингом: кто бы мог подумать лет десять назад, что переименование метода (со всеми вызовами!) можно выполнять почти моментально, практически безоговорочно доверив все действия среде разработки? А сейчас мы постоянно выполняем различные операции рефакторинга, с улыбкой вспоминая времена, когда подобные вещи еще не имели такого сочного названия и выполнялись вручную.

    Обо всем этом можно сказать проще (и притом максимально пристрастно): профессиональное чутье подсказывает автору, что с приходом языковых инструментариев в повседневную практику разработки удастся достичь технологического прорыва, сопоставимого с тем, какой в свое время пережил веб при переходе от CGI-скриптов к динамическим серверным страницам[Теоретически между CGI и, допустим, PHP разницы нет: обе технологии позволяют динамически генерировать HTML. Вопрос только в том, какой ценой. В случае CGI для этого нужно поместить весь генерируемый HTML внутрь операторов вывода какого-нибудь языка программирования (допустим, C++). Главная беда такого подхода даже не в том, что код становится очень тяжело модифицировать. Где вы найдете хорошего веб-дизайнера, улавливающего тонкости оператора printf (или программиста, хорошо разбирающегося в веб-дизайне)? С появлением технологии динамических серверных страниц, построенной на ранее обсуждавшихся принципах метапрограммирования, стала возможной эффективная совместная работа людей со столь разными типами мышления]. С той лишь разницей, что языковые инструментарии охватывают все направления прикладной разработки и позволяют эффективно организовать управление проектом в целом.

    Одна беда - прекрасное пока еще далёко, а потребность в нем ощущается уже сейчас.

    XML и XSLT

    Появившись в далеком 1997 году, XML (eXtensible Markup Language, расширяемый язык разметки) произвел самую настоящую революцию, практически сразу же покорив сердца разработчиков и архитекторов, поскольку совместил в себе, казалось бы, две несовместимые вещи: доступность для восприятия человеком и возможность автоматического распознавания различными программами. Немного позже XML был дополнен технологией XSLT, наделяющей его возможностями метапрограммирования. Вот как выглядит документ XML для разработанного нами языка "Структура статьи в КТ":

    <?xml version="1.0" encoding="UTF-8" ?>
    <статья
    название=”Ориентация на язык”
    автор=”Дмитрий Кириллов”>
    <раздел название=”Введение”/>
    <раздел название=”Языковые инструментарии”>
    <раздел название=”Проектирование DSL”/>
    <раздел название=”Создание редактора”/>

    </раздел>
    </статья>

    Забавно, что практически сразу же после появления XML отовсюду посыпались различные идеи "разработки, основанной на метаданных" (metadata-driven development), созвучные концепции языковых инструментариев. Но... громоздкость конструкций (обратная сторона самоописательности XML), а также, мягко говоря, их ограниченная выразительность зачастую сводят на нет любые попытки построить практичный DSL на основе XML. Чтобы это понять, достаточно представить себе мир, в котором для формирования запроса к базе данных вместо простого и компактного оператора SELECT языка SQL каждый раз пишут монстров, пестрящих угловыми скобками.

    Что же касается возможностей построения метапрограмм-генераторов, то, как показывает опыт развития средств разработки веб-приложений, связка XML/XSLT проиграла сражение на этом поле технологиям динамических серверных страниц (PHP, JSP, ASP.NET и т. п.). Причин много, но главных, пожалуй, две:

    - сложность организации взаимодействия серверных компонентов при разработке средствами XML/XSLT;
    - технологии динамических серверных страниц предоставляют возможность явной реализации принципов метапрограммирования.


  • <<Web 2.c и Web 2.b
    Все материалы номера
    Хорошо информированный оптимист >>