Метапрограммирование на Java

"Ощущения при работе с Java после Ruby примерно такие же, как если бы у тебя были сотни миллионов долларов, а жизнь заставила жить в бедной семье, где всё чисто, опрятно, знают, когда и где распродажи, и так далее - жить можно, и даже неплохо" - вот такой перефразировкой поста [1] начну рассказ о метапрограммировании на Java.

Не так давно мне пришлось разрабатывать настольное приложение на Java с использованием Swing. Оно содержало не очень много бизнес-логики, зато в нем была масса окошек, позволяющих редактировать разные сущности. Каждое окошко я оформлял по образцу MVC, что требовало создания трех классов, плюс класс-обертка ResourceBundle и два файла локализации - итого шесть файлов для одного Swing-окошка.

Разработав первую Swing-форму, я подумал, что Java все-таки несколько многословен. Позже, разработав уже третью форму, почувствовал себя обманутым. Почти половина всего времени у меня уходила только на оформление кода по гайдлайнам и в соответствии с паттернами. О какой продуктивности тут может идти речь?!

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

Отличным примером такой автоматизации является фреймворк Ruby on Rails (RoR). Для тех, кто с ним не знаком, поясню. Чтобы разработать некоторую сущность, программист указывает только, из каких свойств она состоит - типы полей и их названия. После этого RoR сам генерирует схему базы данных, модель с валидаторами, контроллер со всеми основными действиями сущности, все формы для выполнения CRUD-действий, маршруты, файлы локализации, тесты. Весь сгенерированный код уже готов к работе, остается только добавить CSS-стили и бизнес-логику.

 

Чувствуете разницу? На Java вы полчаса оформляете все вручную, а на RoR - просто говорите, что хотите получить, и через пару секунд все готово. В чем же фундаментальное отличие RoR от Java, позволяющее ruby-программисту быть более продуктивным?

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

Конечно, хотелось бы иметь что-то подобное и в Java, раз уж жизнь заставила плотно иметь с ним дело.

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

Например, я бы хотел просто указать, какие элементы управления нужно разместить на Java/Swing-форме, а предварительно разработанный DSL должен сделать все за меня: сгенерить все классы, добавить ActionListener-ы и т. д.

Существует несколько фрэймворков, позволяющих генерировать Java-код за программиста. Самый известный из них - XDoclet [3]. Этот проект был разработан уже довольно давно, когда в Java еще не было аннотаций и метапрограммирование на Java было значительно затруднено. XDoclet использует комментарии к Java-классам и методам, где помещает описание генерируемых классов. Проект хорош тем, что к нему написано большое число генераторов, особенно связанных с J2EE.

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

Еще один генератор кода общего назначения - Cog ([4]). Он позволяет использовать небольшие фрагменты программ на языке Python в качестве генераторов в исходном коде. Cog преобразует файлы достаточно простым способом: он находит куски Python-кода, встроенные в файл, выполняет этот Python-код и затем вставляет результат работы в оригинальный файл. Файл может содержать любой текст вокруг участков Python-кода. Обычно это исходный код какой-нибудь программы.

Например, на входе следующее:

/*[[[cog
import cog
fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing']
for fn in fnames:
 cog.outl("void %s();" % fn)
]]]*/
//[[[end]]]

На выходе будет:

/*[[[cog
import cog
fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing']
for fn in fnames:
 cog.outl("void %s();" % fn)
]]]*/
void DoSomething();
void DoAnotherThing();
void DoLastThing();
//[[[end]]]

У фреймворка Cog очень существенные плюсы: его легко освоить, он поддерживает простой и распространенный язык Python, на нем легко писать. Но и минусы тоже есть. Думаю, вы уже заметили, что в этом исходнике смешан шаблон кода и бизнес-логика (здесь php-программисты злорадно ухмыльнутся).

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

Сейчас, конечно, стоило бы обратиться к flex+bison, antlr или другим инструментам разработки компиляторов... но давайте вспомним, что исходная задача - как можно более простым способом сделать генератор Java/Swing-форм, а вовсе не упражняться в разработке собственных Тьюринг-полных языков программирования.

Для решения этой задачи подойдет любой достаточно развитый скриптовый язык, будь то php, python или perl. Из личных предпочтений я выбрал ruby.

Будущий генератор, по примеру RoR, должен быть основан на паттерне MVC. Модель должна содержать описание генерируемого кода, контроллер будет этот код генерировать, а вид должен содержать шаблон класса в формате erb.

Erb очень прост в использовании. Чтобы обработать шаблон erb, достаточно трех строк кода:

require 'erb'
...
erb = ERB.new(contents, 0, "%()")
processed = erb.result(binding)

Здесь в переменной contents должен содержаться текст erb-темплейта. В самом темплейте можно писать код на ruby примерно так, как это делается в RoR, заключив его предварительно в <%= %>. Все переменные, объявленные в файле-контроллере, вызывающем метод erb.result(), будут доступны и в шаблоне.

Описание генерируемого класса я сделал средствами самого ruby при помощи массивов и словарей. Это избавило меня от необходимости писать парсер DSL, что сэкономило немало времени. Таким образом, например, начало описания класса Java/Swing формы стало выглядеть так:

{:name => "MyView"
 :namespace => "org.generated"
 :text => {:en_US => "My Window", :ru_RU => "Мое окно"}
...

Кнопки на форме описываются так:

:actions =>
[{:name => "btnOk"
  :class => "JButton"
  :text => {:en_US => "OK", :ru_RU => "OK"}
 }
...

Благодаря такой простоте и удобству инструментов, предоставляемых ruby, за два вечера мне удалось написать нужный генератор [5]. Я избавился от вороха рутинных задач, а также от тяжкой необходимости разбираться с чересчур навороченным XDoclet.

Вообще даже такой подход к метапрограммированию на Java не является оптимальным. Написание генераторов исходного кода - это обходной путь решения проблемы полного отсутствия макросов на Java. А ведь есть языки, где эта проблема не стоит вообще. Например, диалекты Lisp (Scheme, Common Lisp) предоставляют возможность не только вводить новые операторы в язык, но и полностью изменить его синтаксис. Например, если вы разрабатываете текстовый редактор, то вначале переделываете Lisp в язык написания текстовых редакторов, а затем делаете свой текстовый редактор. Или вначале переделываете Lisp в язык написания CAD, а затем пишете свой CAD. Примеры у всех на слуху: Emacs и AutoCAD. На платформе Java не так давно появился новый язык Clojure, диалект Lisp, который также предоставляет удобные средства метапрограммирования. В целом, надо полагать, что тенденция к разработке средства метапрограммирования и далее будет усиливаться, что уменьшит количество рутины в разработке ПО.


Литература

1. thesz.livejournal.com/1120988.html

2. www.maxkir.com/sd/languageWorkbenches.html

3. C.Walls, N.Richards, "XDoclet in Action", Manning, 2004.

4. nedbatchelder.com/code/cog/index_ru.html

5. code.google.com/p/java-generator

Дмитрий БУШЕНКО,
d.bushenko@gmail.com

Версия для печатиВерсия для печати

Номер: 

38 за 2010 год

Рубрика: 

Software
Заметили ошибку? Выделите ее мышкой и нажмите Ctrl+Enter!
 

Комментарии

Аватар пользователя Инкогнито
Попробуйте JavaFX)))