Предметно-ориентированные языки (DSL) в проектах на Java

Preview:

DESCRIPTION

Открытый семинар для студентов в компании CUSTIS (14 ноября 2012). Лектор: Иван Гаммель, ведущий разработчик и архитектор Java. Предметно-ориентированные языки (DSL) — современный инструмент для создания лаконичного и выразительного кода, точно описывающего предметную область задачи. На этом семинаре слушатели познакомятся со встроенными предметно-ориентированными языками на Java, на примере реальных задач увидят их практическую применимость и изучат основные архитектурные приемы для построения собственных встроенных DSL. Видеозапись семинара: https://vimeo.com/55268612

Citation preview

Предметно-ориентированные языки в проектах на Java

Иван Гаммель Ведущий разработчик

14 ноября 2012 года

Что такое DSL? DSL, или domain-specific language

(предметно-ориентированный язык) – язык программирования для решения круга задач, ограниченного предметной областью

Примеры: CSS – DSL для описания визуальных стилей

оформления веб-страниц SQL – DSL для построения запросов к СУБД MediaWiki Templates – язык для описания

страниц Википедии

2/57

Особенности DSL Неполнота – на DSL нельзя писать

программы

Зависимость от внешней системы – браузера, СУБД, интерпретатора

DSL бывают как декларативные (CSS), так и императивные (SQL)

DSL ближе к естественным языкам (например, к английскому), чем языки программирования общего назначения

3/57

DSL бывают двух типов – внешние (“external”) и внутренние (“internal”). Этот семинар – о внутренних DSL!

4/57

Что такое встроенный DSL? Встроенный, или «внутренний», DSL –

язык, использующий синтаксические конструкции языка программирования общего назначения

Примеры: SVG – встроенный DSL на основе XML для

векторной графики XML Schema – встроенный DSL на основе XML

для описания форматов XML-файлов Criteria API в JPA (Java) и Linq в C# – DSL для

построения запросов к БД

5/57

Встроенные DSL можно делать и на Java! Этот семинар – об основных приемах построения встроенных DSL на Java.

6/57

Будем изучать DSL на примерах 1. Библиотека доступа к ресурсам приложения

2. Построение запросов при удаленных вызовах

3. Построение объектов модели предметной области

4. Моделирование диаграммы состояний

5. Построение отчета на основе шаблона документа

6. Кастомизация оформления интерфейса

7. Декларативное описание логики интерфейса

8. Написание тестов по функциональным спецификациям

7/57

Что будем использовать в примерах? Шаблоны проектирования (паттерны) Factory, Factory Method Builder Specification Visitor

Особые возможности языка Java Ковариантные типы Generics Аннотации

8/57

Пример 1: Библиотека доступа к ресурсам приложения

Постановка задачи:

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

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

Иногда нужны значения по умолчанию

9/57

Пример 1: Библиотека доступа к ресурсам приложения

Традиционный подход:

Читаем из ResourceBundle, ловим исключение MissingResourceException, выполняем преобразование типа

В лучшем случае пишем обертку, которая занимается поиском ResourceBundle и получением из него значений

Активно перегружаем методы

10/57

Пример 1: Библиотека доступа к ресурсам приложения

Традиционный подход:

11/57

Пример 1: Библиотека доступа к ресурсам приложения

Алгоритм работы:

1. Построить ключ ресурса

2. Найти хранилище, содержащее заданную пару ключ – значение

3. Если ресурс не найден, взять значение по умолчанию

4. Преобразовать к нужному типу и вернуть результат

12/57

Пример 1: Фабричный метод Строим ключ ресурса в перегруженных

фабричных методах

13/57

Пример 1: Свободный интерфейс Получаем значения нужного типа

из хранилища ресурсов

Забываем про геттеры, сеттеры и JavaBeans, пишем по-английски (fluent interface):

OptionalValue<String> value = resources.findBy(key(MyClass.class, “someValue”)); String string = value.asIs(); Integer integer = value.as(Integer.class);

14/57

Пример 1: Реализация интерфейса

Использовали ковариантный тип возвращаемого значения!

15/57

Пример 1: «Зацепление» Если ресурс не найден, взять значение

по умолчанию или бросить исключение – в зависимости от задачи

Аналог сложного предложения – вызов нескольких методов в одной строке (method chaining)

Integer integer = value.or(5).asIs(); boolean flag = value.notNull().as(Boolean.class);

16/57

Пример 1: Как устроено «зацепление»? Для зацепления возвращаем объект

со свободным интерфейсом

17/57

Пример 1: Реализация условия NotNull

18/57

Пример 1: Отличия MandatoryValue и OptionalValue В интерфейсе OptionalValue есть методы or

и notNull

Метод OptionalValue.asIs может вернуть null И это нормально! Мы можем проверить наличие значения вызовом

метода OptionalValue.isSet

MandatoryValue гарантирует, что методы as и asIs вернут непустое значение Исключение MissingValueException мы получим только

тогда, когда отсутствие ресурса – это проблема! 19/57

Пример 1: Какие приемы мы использовали? Паттерн «Фабричный метод» (factory method)

Свободный интерфейс (fluent interface)

Зацепление (method chaining)

20/57

Пример 2: Построение запросов при удаленных вызовах

Постановка задачи:

Типичный программный продукт для бизнеса имеет трехзвенную архитектуру: клиент – сервер приложений – СУБД

Пользователь клиентского ПО может формировать критерии отбора данных для представления в таблице

Нужно передать эти критерии на сервер и получить табличные данные

21/57

Пример 2: Построение запросов при удаленных вызовах

Идея: почему бы не использовать Detached Criteria

из Hibernate?

Проблемы: Дыра в абстракции: клиент получает зависимость

от Hibernate Дыра в безопасности: можно попросить

что-нибудь лишнее и получить это Аналогия с SQL Injection

Плохая идея!

22/57

Пример 2: Построение запросов при удаленных вызовах

Решение: Реализуем модель запроса (Query Object)

для передачи данных с клиента на сервер Транслируем модель запроса в запрос к СУБД

на сервере Строим модель запроса с помощью DSL

Предполагаемый вид запроса:

Criteria<Document> criteria = from(Document.class) .select(start(5), count(10)) .where(property(“date”).between(startDate, endDate) .and(property(“status”).equals(PROCESSED)));

23/57

Пример 2: Строим предикаты с помощью паттерна «Спецификация» Используем зацепление:

24/57

Пример 2: Используем фабричные методы для конкретных условий

25/57

Пример 2: Задаем постраничную разбивку с помощью токенов Было:

select(10, 5)

Можно перепутать параметры при вызове

Стало: select(count(5), start(10)) и select(start(10), count(5))

Токены позволяют контролировать семантику входных параметров метода на уровне компилятора

26/57

Пример 2: Реализация с помощью токенов Используем зацепление с return this:

27/57

Пример 2: Реализация токена для постраничной разбивки

Мы можем получить экземпляр токена с помощью фабричного метода

28/57

Пример 2: А что на сервере? А на сервере с помощью instanceof

разбираем полученный объект и строим критерий JPA или SQL-запрос

А можем и не разбирать, если храним данные в памяти: У нас есть метод matches! Переберем объекты и проверим на соответствие

критерию

Но это уже совсем другая история…

29/57

Пример 2: Использованные приемы Зацепление c return this

Паттерн «Спецификация» (Specification)

Токены (tokens)

Фабричные методы, свободный интерфейс

30/57

Немного о зацеплении: мы уже видели return this и ковариантные типы. Что еще можно сделать?

31/57

Зацепление и наследование Ковариантные типы возвращаемых

значений – подкласс может уточнить тип возвращаемого значения

32/57

Зацепление и наследование Generics: параметризация самим собой –

уточнение типа можно сделать уже в суперклассе!

33/57

«Зацепление» и неизменяемость (immutability) А почему бы и нет? Можно в любой момент получить экземпляр

класса в нужном состоянии, не боясь испортить исходный объект

Используем return new MyObject(…); Пример: java.math.BigDecimal

34/57

Пример 3: Паттерн Builder Builder («строитель») – класс,

предназначенный для построения экземпляров другого класса

Формулируем с помощью DSL задачу и командуем: build()

Currency currency = aCurrency().withName(“Рубль”).build();

35/57

Пример 3: Исходный код

36/57

Когда использовать Builder? Сложная логика формирования объектов

Создание объектов по образцу

Построение иерархий объектов

Отложенное создание объекта

37/57

Пример 4: Модель диаграммы Моделируем получение событий

от внешней системы

Некоторые события могут быть обработаны только после получения других событий

События связаны друг с другом разными способами

38/57

Пример 4: Используем «слова» DSL Вспомогательные объекты (токены) помогут

нам точнее сформулировать, что мы хотим сделать

Передаем их в качестве параметра в методы свободного интерфейса

Можно использовать перечисления или строки

А можно – объекты, созданные с помощью фабричных методов

Активно используем import static

39/57

Пример 4: Исходный код

40/57

Пример 5: Построение отчета на основе шаблона документа При заполнении шаблона нужно выполнить

множество действий: Несколько строк нужно изменить, несколько –

вставить… А потом еще несколько изменить и вставить!

Проблема: шаблон нужно изменять в определенном порядке или вычислять смещения строк и столбцов после вставки

41/57

Пример 5: Builder, токены и отложенная модификация объекта Используем паттерн Builder

для построения сценариев изменения шаблона

Сначала построим все сценарии, а потом их выполним К моменту выполнения нам будут известны все

смещения!

42/57

Пример 5: Исходный код Скрываем сложную логику вычисления смещения

строк в таблице Excel внутри реализации паттерна Builder в классе ExcelTxInsert

Tx – потому что похоже на транзакцию: выполняем вычисления только после того, как вызвали commit

43/57

Пример 6: Настройка оформления Проблемы: Много разных компонентов Заранее не известно, какие возможности

визуального оформления будут использованы

Решение: Используем паттерн Visitor: опишем интерфейс

Стиль, реализации которого будут применять различные стили к компонентам

Это пример расширяемости DSL!

44/57

Пример 6: Исходный код Конкретные стили могут быть

реализованы не только в библиотеке DSL, но и в прикладном коде:

45/57

Пример 7: Декларативное описание логики интерфейса Проблема: запуск простого алгоритма

в интерфейсе очень многословен Нужно создать кнопку Активировать горячие клавиши Подписаться на события кнопки и вызвать метод

с реализацией алгоритма

Решение: Используем аннотации метода для описания

кнопок и событий, его запускающих

46/57

Пример 7: Как это выглядит? Для поддержки таких описаний нужен достаточно мощный фреймворк:

Описание действия «Создать шаблон проводки» включает: Контекст выполнения действия (DEFAULT_SCOPE – по умолчанию) Расположение кнопки на панели инструментов (INHERIT – рядом

с другими кнопками действий, определенных в этом классе) Реакцию на события в таблице шаблонов проводок (нажатие

клавиши INSERT)

47/57

Пример 7: Аннотации как прием для построения DSL Можно использовать вложенные аннотации

Токены – примитивные типы, перечисления

Для сложной логики можно использовать ссылки на классы, например:

@Validator(EmailValidator.class)

Можно сочетать с другими приемами: Декларативное описание задачи на аннотациях Анализ и трансляция в императивный код

в реализации паттерна Builder

48/57

Пример 8: «Дано» и «Доказать» Test-Driven Development (TDD) – сначала

пишем тесты, потом код

Behavior-Driven Development (BDD) – сначала записываем истории пользователей (user stories) и сценарии работы, потом пишем тесты

DSL и BDD – записываем сценарии сразу в виде тестов

given – «дано», assert – «доказать»

49/57

Пример 8: Пишем тесты Используем JUnit или TestNG

Используем Builder для построения объектов

Используем конструкции given и assert

50/57

Пример 8: Исходный код теста Тестируем на конкретной реализации репозитория,

имеющей метод assertExists(Matcher matcher):

С использованием Criteria API из примера 2 можно и так:

assertExists(anyDeal().withKind(TOD)).in(deals);

Строим критерий, после чего вызываем findByCriteria у репозитория и проверяем результат

51/57

Резюме: основные приемы построения DSL Свободный интерфейс (fluent interface)

Зацепление (method chaining)

Токены (tokens)

Аннотации

Паттерны ООП: Builder Specification Visitor

52/57

DSL или API? DSL – разновидность API: Можно оформить в виде библиотеки Подразумевается широкое повторное

использование конструкций языка

53/57

Когда использовать DSL? Перевод в код функциональных требований,

спецификаций и пользовательских сценариев работы, сформулированных по единому шаблону

Замена повторяющихся конструкций в коде, для которых явно определена предметная область

NB: важно наличие «лексического ядра» – словаря, общего для всех повторяющихся конструкций и спецификаций

54/57

Заключение Рассмотренные приемы для создания DSL

достаточны для применения в собственных проектах

DSL делает ваш код читабельнее и короче

DSL избавляет от формализма командную работу

Современные практики разработки ПО поощряют использование DSL

55/57

Ссылки по теме Building a fluent API (internal DSL) in Java,

Gabrielle Carcassi

«Приемы объектно-ориентированного проектирования. Паттерны проектирования», Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес (GoF)

«Предметно-ориентированное проектирование (DDD). Структуризация сложных программных систем», Эрик Эванс

«Предметно-ориентированные языки программирования», Мартин Фаулер

56/57

Спасибо! Вопросы?

Иван Гаммель igammel@custis.ru ivan-gammel.moikrug.ru/

57/57

Recommended