О чем вообще речь
В этом посте я хочу порассуждать отвлеченно на тему разработки приложений. Сначала я задумал написать просто про генерацию кода, но по мере обдумывания темы у меня родилось много мыслей, которыми тоже хочу поделиться. Поэтому получилось чуть шире, чем просто про DSL.
Что такое DSL (Domain Specific Language) и кодогенерация
DSL — это язык, специфичный для конкретной доменной области. Т.е. это язык, который оперирует понятиями данной области напрямую. Обычно противопоставляется языкам общего назначения. В принципе ничто не мешает быть языку быть просто формальным синтаксисом, никак не интерпретируемым компьютером, но пользы от такого языка не очень много. Компьютерный язык обычно подразумевает обработку каким-либо образом, поэтому к DSL неплохо бы иметь какой-нибудь интерпретатор. Соответственно есть два стандартных подхода — интерпретация и компиляция. С интерпретацией более-менее ясно, а с компиляцией история следующая. Можно конечно транслировать сразу в инструкции процессора или на худой конец в ассемблер, но зачем, если можно «писать» нормальный код, в смысле компилировать в текст высокоуровневого языка, который потом преобразуется своим компилятором в нечто, запускаемое не компьютере. Поэтому чаще и говорят «кодогенерация», а не компиляция, хотя последний термин тоже корректен и используется.
Производительность труда
Если взять разработку приложений, то главной проблемой я считаю низкую производительность, т.е. «количество продукта» на затраченные усилия. В принципе похожая проблема встречается во всех отраслях промышленности, и есть как общие методы решения, так и специфичные. У нас есть много разных вещей для поднятия этой самой производительности — высокоуровневые языки, мощные IDE, continious integration tools, scrum, canban, coffee points, coffee ladies и много чего еще. Но тем не менее разработка продуктов занимает кучу времени. Особенно это заметно, когда то, что нужно сделать можно легко описать словами за несколько минут, а сделать — занимает недели. Существенный разрыв между «что» и «как». «Что делать» — просто и понятно, «как делать» — просто, понятно, но долго. Я хочу сделать «как» — быстро, а в идеале вообще не делать. Короче, декларативный подход.
Уровни абстракции
Есть очень полезное понятие — уровень абстракции. Оно помогает структурировать приложения. Допустим у нас есть приложение для некоторой предметной области. С одной стороны (вверху) есть понятия из этой предметной области, которые так или иначе будут фигурировать в приложении, с другой стороны есть язык программирования общего назначения (внизу), в котором есть байты, типы, методы и тому подобные элементы, не имеющие ничего общего с предметной областью (не будем спускаться ниже до операционной системы, электрических импульсов, транзисторов, молекул, атомов, протонов, кварков...). Работа программиста как раз и состоит в том чтобы увязать эти два слоя или заполнить область на картинке (левая картинка). Если приложение большое и доменная область достаточно «далеко», то в приложении возникают различные промежуточные уровни абстракции, иначе можно не совладать со сложностью (правая картинка).
Уровни, конечно, возникают, но возникают они логически. И надо прикладывать определенные усилия, чтобы код тоже поддерживал уровни. Это особенно сложно, если язык один и все запущено в одном процессе. Ведь ничто не мешает вызвать метод из уровня 1 на уровне 3. Да и функции или классы обычно не маркируются уровнем абстракции. Что нам по этому поводу предлагает DSL с кодогеном? Нам по-прежнему надо заполнить ту же область. Соответственно, верхнюю часть заполняем нашим языком, а нижнюю сгенерированным кодом:
В отличие от предыдущего примера уровень здесь непроницаем, т.е. из генерированного кода нельзя вызвать инструкции DSL (особенно если их там нет). Не будем рассматривать случаи, когда генератор делает код на том же DSL… Еще один важный момент здесь — это то, что generated code можно рассматривать как скомпилированный, в том смысле, что он создается автоматически и смотреть в него незачем. При условии, что генератор уже написан (и хорошо протестирован). Т.е. написав язык и генератор к нему можно значительно сузить область приложения. Это особенно ценно при разработке нескольких приложений в этой сфере или при постоянном изменении одного.
Управление «усложнением»
Давайте представим себе ситуацию, которая, как мне кажется, встречается довольно часто. Допустим вы получаете заказ на разработку некоторой системы. Вам приносят идеальную спецификацию и вы придумываете идеальную архитектуру системы где все прекрасно, компоненты, интерфейсы. инкапсуляция и много других не менее прекрасных паттернов. Возьмем конкретный пример — интернет-магазин велосипедов. Вы написали согласно спецификации интернет-магазин и все счастливы. Магазин процветает и задумывается о расширении бизнеса, а именно начать еще торговать скутерами и мотоциклами. И вот они приходят к вам и просят доработать магазин. У вас была прекрасная архитектура, заточенная на велосипеды, но теперь надо перетачивать. С одной стороны скутеры и мотоциклы похожи на велосипеды, и у тех и у тех есть запчасти, аксессуары, сопутствующие товары, но есть и различия.
Система в целом остается такой же, но часть функций должна поддерживать еще новые типы объектов, или должны появиться отдельные функции для новых типов объектов.
Произошло усложнение доменной области, т.е. вместо только велосипедов теперь надо поддерживать велосипеды, скутеры и мотоциклы. Наша системы тоже должна быть усложнена. Я думаю, что в общем случае сложность программной системы соответствует сложности моделируемой системы. При этом существуют минимально возможный уровень сложности при котором все еще можно решить задачу. (Верхнего уровня не существует — можно придумать бесконечно сложное решение для любой проблемы). Я считаю, что надо стремиться к минимальному уровню сложности, так как из всех возможных решений самое простое — самое лучшее. Короче, код должен быть простым.
Вернемся к нашему интернет-магазину. Пусть есть некая функция, которая написана для велосипеда. Теперь она должна работать и для новых типов.
public void process(Bicycle b) {
genericCode
specificForBicycle
}
для этого должен быть specificForMotobike код внутри. Какие есть варианты решения?
Copy/paste
public void process(Motobike b) {
genericCode
specificForMotobike
}
Скопировали метод, заменили специфичный для типа код и все. Просто, но есть проблема. Если надо менять genericCode, то надо менять то же самое в нескольких местах, а это время, ошибки…
If/else
public void process(Object b) {
genericCode
if(b instanceof Bicycle) {
specificForBicycle
} else if(b instanceof Motobike) {
specificForMotobike
}
}
Наставили условий и все готово. Немного лучше, чем copy/paste, но опять есть проблема. А завтра они захотят продавать квадроциклы и придется по всему коду искать такие куски и добавлять еще один else.
Абстрактный метод
abstract void specific()
public void process(Vehicle b) {
genericCode
b.specific()
}
В этом месте вызывается абстрактный метод, который реализован для каждого типа. В принципе это может оказаться приемлемым вариантом, а может и существенно усложнить систему. Многоэтажные иерархии наследования с кучей переопределенных методов, когда нелегко разобраться какой конкретно метод вызывается — нередкая ситуация.
DSL и генерация кода
DSL разрабатывается таким образом, что все особенности типов можно описать. В генераторе кода пишутся шаблоны, которые применяются к описанию типов и получается код как в copy/paste
Шаблон:
public void process("TYPE" b) {
genericCode
"SPECIFIC CODE"
}
DSL:
type Bicycle:
property A, ( description, value, links ...)
type Motobile:
property B,
property C,
Дальше для каждого типа из DSL шаблон трансформируется в конкретный код. Из моего опыта сложно сразу написать язык, который бы без изменений поддерживал новые сущности, но изменения языка и генератора обычно небольшие и простые. Вообще подход следующий — генерируется много простого кода, который легко читать и понимать, и неважно что файлов получается много и они могут быть по несколько тысяч строк. Ведь это же не руками писать.
DSL вначале или формализованная спецификация
Здесь я подхожу к самому главному. (до этого было вступление :) Как обычно выглядит процесс начала проекта? Пишутся спецификации, рисуются диаграммы, прорабатывается архитектура, этапы проекта. И когда это все сделано, начинают писать код. Спецификации — это документы в свободной форме. Почему бы спецификации не быть формализованной? Моя основная идея — сначала разрабатывать язык описания системы в терминах доменной области. Это будет частично и описание архитектуры, и частично формализованной спецификацией. При этом заказчику будет понятен язык, так как он непосредственно оперирует терминами предметной области, и он тоже сможет принять участие в разработке системы. Идея, конечно, не моя. В литературе такой подход называется Domain-Driven Design (DDD). Я лишь утверждаю, что подход DDD хорошо получается с DSL и генерацией кода.
Формализация означает возможность автоматической обработки. Можно добавить различные проверки на консистентность, непротиворечивость. С другой стороны, разработчики системы имеют готовую формализованную декларацию что должно быть. Остается написать преобразователь
Не все так гладко
Конечно, не все так просто и гладко. Как у любого другого подхода здесь есть свои проблемы и недостатки.
- Не всегда понятно что генерировать. Надо представлять себе конечный систему. Ведь не весь код генерируется и надо понимать что будет сгенерировано, а что написано руками, и как это все будет работать вместе. Иногда проще сначала написать все вручную (держа в голове будущую генерацию), а потом часть кода вытащить в шаблоны и генераторы.
- Вторая проблема — баланс сгенерированного и ручного кода. Нет смысла выносить в шаблон код, который фактически не параметризован и одинаков всегда. Плохой практикой является одновременное использование подходов из примеров выше.
- Зависимости между ручным и генерированным кодом. Не надо делать так, чтобы ручной код ломался при изменении DSL. (текста на DSL)
- «Повреждение» мозга кодогенерацией. Написание кодогенераторов несколько отличается от написания обычных программ. Использование «не того» стиля приводит к написанию «не очень» кода. Спасает ревью и «здоровые» коллеги.
- Еще один момент, с которым я столкнулся — сложно убедить заказчика в правильности подхода. Мол, раньше обходились как-то, и дальше нормально будем жить, а ты тут со своими идеями. И вообще, где поддержка скутеров, которую ты должен был вчера сделать? Иди работай.
- Вы видели вакансии DSL-разработчик? Но тут, наверное, так же как устроиться программистом на Haskell. Устраиваетесь программистом на Java(C++, Perl, Python, etc). Убеждаете, что
HaskellDSL — это круто. И вот вы уже DSL-разработчик.
Средства для разработки DSL и написания генераторов кода
Все что я написал до этого имело бы мало практического смысла без нормальных средств разработки. К счастью такие средства есть. Средства есть разные, но мой выбор Eclipse Xtext. Самое главное, что есть в xtext — интеграция в Eclipse IDE, а именно есть все стандартные свойства — подсветка синтаксиса, ошибки и предупреждения, content assist, quick fix. Это что называется «из коробки». А дальше на что фантазии хватит. Я думаю, что сделаю еще несколько практических постов по теме, если будет интерес.
Заключение
Я думаю, я не открыл Америки. Многое из того, что я написал — банальные вещи. Но с другой стороны, я считаю тема DSL и генерации кода недостаточно раскрыта, поэтому я решил попробовать свои силы в просвещении. Да и про Eclipse Xtext не так много слышали, а тем более используют.