Главная » Хабрахабр » Kotlin DSL: Теория и Практика

Kotlin DSL: Теория и Практика

Разработка тестов приложения — не самое приятное занятие. Этот процесс занимает долгое время, требует большой концентрации и при этом крайне востребован. Язык Kotlin дает набор инструментов, который позволяет довольно легко построить собственный проблемно-ориентированный язык (DSL). Есть опыт, когда Kotlin DSL заменил билдеры и статические методы для тестирования модуля планирования ресурсов, что превратило добавление новых тестов и поддержку старых из рутины в увлекательный процесс.

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

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

Дальнейшее повествование ведется от его лица. Статья основана на докладе Ивана Осипова (i_osipov) на конференции JPoint. Основной продукт компании – CUBA, платформа для разработки энтерпрайза и различных веб-приложений. Иван работает программистом в компании Haulmont. Так сложилось, что последние три года Иван так или иначе работает с планировщиками, и конкретно в Haulmont в течение года они этот самый планировщик тестируют.
Для желающих позапускать примеры — держите ссылку на GitHub. В том числе на этой платформе делаются и аутсорсинговые проекты, среди которых недавно был проект в области образования, в котором Иван занимался построением расписания для образовательного учреждения. Открывайте код и вперед! По ссылке вы найдете весь код, который сегодня мы с вами будем разбирать, запускать и писать.

Сегодня мы обсудим:

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

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

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

Данные проходят валидацию, фильтрацию, затем строятся учебные группы. Этот процесс можно представить следующим образом: на входе имеются какие-то данные с общей моделью, на выходе – расписание. На основе построенных групп и на основе каких-то других данных мы размещаем занятие. Имеется в виду предметная область расписания для учебного учреждения. Сегодня мы будем говорить только про последний этап – про размещение занятий.

Немного про тестирование планировщика.

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

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

Напишем тест:

Возможно, это несколько примитивные тесты такого вида: создаешь класс, в нем создаешь метод, помечаешь его аннотацией Test. Давайте напишем самый простой тест для того, чтобы вы в общем понимали картину.
Что первое приходит на ум, когда думаешь про тестирование? Более-менее стандартный процесс. В итоге, мы пользуемся возможностями JUnit, и инициализируем какие-то данные, значения по умолчанию, затем специфические для теста значения, делаем все то же самое для остальной части модели, и, наконец, создаем объект-планировщик, передаем в него наши данные, запускаем, получаем результаты и проверяем их. Первое, что приходит на ум, это возможность все вынести в статические методы. Но в нем, очевидно, есть дублирование кода. Раз есть куча значений по умолчанию, почему бы это не скрыть?

Это хороший первый шаг по пути уменьшения дублирования.

Тут у нас появляется паттерн-строитель, в котором где-то под капотом инициализируется значение по умолчанию, и тут же инициализируются специфичные для теста значения. Глядя на это, ты понимаешь, что хотелось бы модель держать более компактно. Представьте 200 тестов – 200 раз придется написать эти три строчки. Становится уже лучше, однако, мы все еще пишем boilerplate-код, и пишем его мы каждый раз заново. Развивая идею, мы приходим к некоторому пределу. Очевидно, хотелось бы от этого как-то избавиться. Так, например, мы можем создать паттерн-билдер вообще для всего.

Если взглянуть подробно на этот пример и детально его разобрать, то окажется, что пишется большое количество ненужного кода. Можно создавать планировщик с нуля и до конца, задавать все нужные нам значения, запускать планирование и все здорово. Хотелось бы сделать тесты более читаемыми, чтобы можно было взглянуть и сразу понять, не вникая в паттерны и так далее.

Несложная математика подсказывает, что тут на 55% больше букв, чем нам необходимо, и хотелось бы как-то от них уйти. Итак, у нас есть какое-то количество ненужного кода.

Иногда, если мы не предпринимаем каких-то усилий, читаемость либо оставляет желать лучшего, либо получается приемлемо, но нам бы хотелось еще лучше. Спустя некоторое время поддержка наших тестов оказывается дороже, потому что кода поддерживать нужно больше. Благодаря этому, мы повышаем уровень вхождения в тестирование нашего приложения. Возможно, впоследствии мы начнем добавлять какие-то фреймворки, библиотеки, чтобы тесты писать было проще. Здесь у нас и так сложное приложение, уровень вхождения в его тестирование значителен, а мы его еще сильней повышаем.

Идеальный тест

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

Мы строим расписание, у нас есть какие-то данные, и результаты планировщика проверяются. Представим, что есть некоторая декларация, в которой мы скажем, что это тест с определенным названием, и хочется использовать пробел для разделения слов в названии, а не CamelCase. Инициализировать данные хотелось бы максимально очевидно для читателя. Так как мы работаем в основном с Java, и весь код основного приложения написан на этом языке, хочется иметь еще и совместимые возможности в тестировании. Например, создавать студентов, преподавателей, и описывать, когда они доступны. Хочется инициализировать некоторые общие данные и часть модели, которая нам необходима. Вот это — наш идеальный пример.

Domain Specific Language

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

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

Варианта у нас три. Снова взглянем на наш идеальный пример и подумаем, какой язык выбрать.

Замечательный, динамичный язык, который отлично показал себя в построении проблемно-ориентированных языков. Первый вариант – Groovy. Eще есть Scala, которая имеет огромное количество возможностей для реализации чего-то своего. Снова можно привести пример build файла в Gradle, которым многие из нас пользуются. Я бы не хотел разводить войн и сравнивать Kotlin с чем-то другим, скорее, это остается на вашей совести. И наконец, есть Kotlin, который нам также помогает строить проблемно-ориентированный язык, и сегодня именно о нем пойдет речь. Когда вы захотите сравнить это и сказать, что какой-то язык лучше, вы сможете вернуться к этой статье и легко увидеть разницу. Сегодня я покажу вам то, что есть в Kotlin для разработки проблемно-ориентированных языков.

Что дает нам Kotlin для разработки проблемно-ориентированного языка?

На этапе компиляции обнаруживается большое количество проблем, и это очень сильно спасает, особенно в том случае, когда не хочется в тестах получать проблемы, связанные с синтаксисом и написанием.
Затем, есть отличная система вывода типов, которая приходит из Kotlin. Во-первых, это статическая типизация, и все отсюда вытекающие. Это замечательно, потому что нет потребности снова и снова писать какие-то типы, все выводится компилятором на ура.

На мой субъективный взгляд, поддерживать DSL намного проще, чем поддерживать утилитные классы. В-третьих, есть отличная поддержка среды разработки, и это неудивительно, ведь та же компания, делает основную на сегодня среду разработки, и она же делает Kotlin.
Наконец, внутри DSL, очевидно, мы можем использовать Kotlin. Что я понимаю под «лучше»: у вас получается несколько меньше синтаксиса, который вам необходимо писать, — тот, кто будет читать ваш проблемно-ориентированный язык, будет быстрее это воспринимать. Как вы увидите далее, читаемость оказывается немного лучше билдеров. Но на самом деле, реализовать проблемно-ориентированный язык намного проще, чем изучить какой-то новый фреймворк. Наконец, написать свой велосипед намного веселее!

Я напомню еще раз ссылку на GitHub, если вы захотите писать демки дальше, то вы можете зайти и забрать код по ссылке.

Проектирование идеала на Kotlin

Перейдем к проектированию нашего идеала, но уже на Kotlin. Взглянем на наш пример:

И поэтапно начнем его отстраивать.

У нас есть тест, который превращается в функцию в Kotlin, которую можно именовать, используя пробелы.

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

То же самое происходит с большим количеством конструкций, так как мы все-таки работаем в Kotlin. Schedule у  нас превращается в блок.

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

Наш student превращается в некоторый блок, в котором идет работа со свойствами, с методами, и это мы дальше с вами будем разбирать.

Здесь у нас появляются некоторые вложенные блоки. Наконец, преподаватели.

Нам нужны проверки на совместимость с Java-языками – и да, Kotlin совместим с Java. В коде ниже мы переходим к проверкам.

Арсенал разработки DSL на Kotlin

Здесь я привел табличку может быть, в ней перечислено все, что необходимо для разработки проблемно-ориентированных языков в Kotlin. Перейдем к перечню инструментов, которые у нас есть. Можно время от времени к ней возвращаться и освежать память.

В таблице приведено некоторое сравнение проблемно-ориентированного синтаксиса и обычного синтаксиса, который имеется в языке.

Лямбды в Kotlin

val lambda: () -> Unit =

Лямбды обозначаются следующим образом: (типы параметров) -> возвращаемый тип. Начнем с самого базового кирпичика, который у нас есть в Kotlin – это лямбды.
Сегодня под типом лямбды я буду подразумевать просто функциональный тип.

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

Если мы хотим передать какой-то параметр, во-первых, мы должны описать это в типе.
Во-вторых, мы имеем доступ к идентификатору по умолчанию it, которым мы можем пользоваться, однако, если нас это как-то не устраивает, можно задать своё имя параметра и пользоваться ими.

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

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

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

Тем, кто знаком с Groovy, это должно быть знакомо. Если в скобках не осталось ничего, скобки мы можем упразднить.

Абсолютно везде. Где это применяется? То есть те самые фигурные скобки, про которые мы с вами уже говорили, их мы и используем, это и есть те самые лямбды.

Вы встретите какие-то другие названия, например, lambda with receiver, и отличаются они от обычных лямбд при объявлении типа следующим образом: слева мы дописываем какой-то класс контекста, это может быть любой класс. Теперь посмотрим на одну из разновидностей лямбд, я их называю лямбды с контекстом.

Это нужно для того, чтобы внутри лямбды мы имели доступ к ключевому слову this – это самое ключевое слово, указывает нам на наш контекст, то есть на некоторый объект, который мы связали с нашей лямбдой. Для чего это нужно? Так, например, мы можем создать лямбду, которая будет выводить некоторую строку, естественно, мы воспользуемся классом строки для объявления контекста и вызов такой лямбды будет выглядеть вот так:

Однако, совсем передать контекст мы не можем, то есть лямбда с контекстом требует – внимание! Если вам хочется передать контекст в качестве параметра, вы можете это точно также сделать. Что будет, если мы начнем передавать лямбду с контекстом в какой-то метод? – контекста, да. Вот посмотрим снова на наш метод exec:

Переименуем его в метод student – ничего не изменилось:

Так мы постепенно движемся к нашей конструкции, конструкции student, которая под фигурными скобками скрывает всю инициализацию.

У нас есть какая-то функция student, которая принимает лямбду с контекстом Student. Давайте в ней разберемся.

Очевидно, нам нужен контекст.

Здесь мы создаем объект и на нем же запускаем эту лямбду.

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

Благодаря этому, внутри лямбды мы получаем доступ к ключевому слову this – то, ради чего, наверное, и существуют лямбды с контекстом.

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

Опять же, если у нас есть не только проперти, а еще есть какие-то методы, мы можем их также вызывать, это выглядит довольно естественно.

Применение

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

Резюмируя по лямбдам – у нас есть лямбды обычные, есть с контекстом, и теми, и другими можно пользоваться.

Операторы

В Kotlin есть ограниченный набор операторов, который вы можете переопределять, используя соглашения и ключевое слово operator.

Допустим, мы говорим, что преподаватель работает по понедельникам с 8 утра в течение 1 часа. Посмотрим на преподавателя и на его доступность. 00 в течение 1 часа. Еще мы хотим сказать, что, кроме этого одного часа, он работает с 13.  Как это можно сделать? Хочется выразить это с помощью оператора +.

Это значит, что есть некоторый класс, который так и называется, и в этом классе объявлен метод monday. Имеется некоторый метод availability, который принимает лямбду с контекстом AvailabilityTable. нужно к чему-то прикрепить наш оператор. Этот метод возвращает DayPointer, т.к.

Это указатель на таблицу доступности некоторого преподавателя, и день в его же расписании. Давайте разберемся в том, что такое DayPointer. Также у нас есть функция time, которая будет так или иначе превращать какие-то строки в целочисленные индексы: в Kotlin у нас для этого есть класс IntRange.

Для этого в классе DayPointer можно создать наш оператор. Слева есть DayPointer, справа есть time, и нам хотелось бы их объединить оператором +. Ее реализация немного отличается, и сейчас мы в этом разберемся.
В Kotlin есть понятие синглтона, встроенное прямо в язык. Он будет принимать диапазон значений типа Int и возвращать DayPointer для того, чтобы мы цепочкой могли снова и снова склеивать наш DSL.
Теперь взглянем на ключевую конструкцию, с которой все начинается, с которой начинается наш DSL. Если мы создаем метод внутри синглтона, то можно обращаться к нему так, что нет необходимости снова создавать инстанс этого класса. Для этого вместо ключевого слова class используется ключевое слово object. Мы просто обращаемся к нему как к статическому методу в классе.

Если  взглянуть на результат декомпиляции (то есть, в среде разработки прокликать Tools –> Kotlin –> Show Kotlin Bytecode –> Decompile), то можно увидеть следующую реализацию синглтона:

Представим, что у нас есть некоторый класс А, у нас есть его инстанс, и мы хотели бы словно запускать этот инстанс, то есть вызывать круглые скобки у объекта этого класса, и мы можем это сделать благодаря оператору invoke. Это всего лишь обычный класс, и ничего сверхъестественного здесь не происходит.
Имеется еще один интересный инструмент – это оператор invoke.

Если же мы передадим в этот оператор лямбду с контекстом, то у нас получится вот такая конструкция. По сути, круглые скобки позволяют нам вызывать метод invoke и имеет модификатор operator.

Создавать каждый раз инстансы то еще занятие, поэтому мы можем совместить предыдущие знания и текущие.

Получается единая точка входа в наш DSL, и, как следствие, получается та же самая конструкция – schedule с фигурными скобками. Сделаем синглтон, назовем его schedule, внутри него мы объявим оператор invoke, внутри создадим контекст, а принимать он будет лямбду с контекстом вот тем самым, который мы здесь же и создаем.

Отлично, про schedule мы поговорили, давайте взглянем на наши проверки.
У нас есть преподаватели, мы построили какое-то расписание, и хотим проверить, что в расписании этого преподавателя в определенный день в определенном занятии есть какой-то объект, с которым мы будем работать.

Хотелось бы использовать квадратные скобки и обращаться к нашему расписанию способом, визуально похожим на доступ к массивам.

Сделать это можно с помощью оператора: get / set:

В случае оператора set нужно дополнительно передать значения в наш метод: Здесь мы не делаем ничего нового, просто следуем соглашениям.

Итак, квадратные скобки для чтения превращаются в get, а квадратные скобки, через которые мы присваиваем, превращаются в set.

Демо: object, operators

Дальнейший текст можно или читать, или смотреть видео по ссылке. У видео есть четкое время начало, но не указано времени окончания — в принципе, однажды начав, можно досмотреть его до конца статьи.

Для удобства я кратко изложу суть видео прямо в тексте.

У нас есть некоторый объект schedule, и если мы через ctrl+b перейдем к его реализации, то мы увидим все, о чем я перед этим говорил. Давайте напишем тест.

Внутри объекта schedule мы хотим проинициализировать данные, затем выполнить какие-то проверки, и в рамках данных мы хотели бы сказать, что:

  • наше учебное заведение работает с 8 утра;
  • есть некоторый набор предметов, для которых мы будем строить расписание;
  • есть некоторые преподаватели, у которых описана какая-то доступность;
  • есть студент;
  • в принципе для студента нам нужно сказать только то, что он изучает какой-то определенный предмет.

В этом демо я буду указывать все в качестве индексов, то есть rus – это индекс 0, математика – это индекс 2. И здесь проявляется один из минусов Kotlin и проблемно-ориентированных языков в принципе: довольно сложно адресовать какие-то объекты, которые мы создали раньше. Он не просто на работу ходит, а чем-то занимается. И преподаватель естественно, тоже что-то ведет. Продолжим разбирать DSL. Для читателей этой статьи я хотел бы предложить еще один вариант адресации, вы можете завести уникальные теги и по ним сохранять сущности в Map, а когда нужно обратиться к какой-то из них, то по тегу вы всегда можете её найти.

Здесь что нужно отметить: во-первых, у нас есть оператор +, к реализации которого мы также можем перейти и увидеть, что у нас на самом деле есть класс DayPointer, который помогает нам связывать это все с помощью оператора.

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

Ивент в себя инкапсулирует набор свойств, например: что имеется студент, преподаватель, в какой день на какой урок они встречаются. То есть у нас это коллекция ивентов.

Продолжим писать тест дальше.

Здесь, опять же, мы пользуемся оператором get, перейти к его реализации не так просто, но мы можем это сделать.

Мы хотели проверки, реализованные на Kotlin, и мы перебирали эти вот ивенты: По сути, мы просто следуем соглашению, благодаря чему и получаем доступ к этой конструкции.
Давайте вернемся к презентации и продолжим разговор про Kotlin.

Хочется раскладывать этот ивент на набор свойств, словно кортеж. Ивент – это, по сути, инкапсулированный набор из 4 свойств. В русском языке такая конструкция называется мульти-декларации (я нашел только такой перевод), или destructuring declaration, и работает это следующим образом:

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

Работает это потому, что у нас есть метод componentN, то есть это метод, который генерируется компилятором благодаря модификатору data, который мы пишем перед классом.

Нас интересует именно метод componentN, генерируется на основе перечисленных в списке параметров primary-конструктора свойств. Вместе с этим нам прилетает большое количество других методов.

Если бы у нас не было модификатора data, необходимо было бы вручную написать оператор, который будет делать все то же самое.

Итак, у нас какие-то методы componentN, и они, раскладываются вот в такой вызов:

По сути, это синтаксический сахар над вызовом нескольких методов.

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

Это можно сделать с помощью так называемого typealias или псевдонима типа. Не нужно никакого дополнительного класса: можно взять матрицу булевских значений и переименовать для большей очевидности. Если вы возьмете и availability переименуете обратно в матрицу булевских значений, вообще ничего не изменится. К сожалению, никаких дополнительных бонусов мы от этого не получаем, это просто переименование. Код как работал, так и будет работать.

Давайте взглянем на преподавателя, вот как раз на эту самую доступность, и поговорим о нем:

:-). У нас есть преподаватель, и у него вызывается метод availability (вы еще не потеряли нить рассуждений? То есть, преподаватель — это какая-то entity, у которой есть класс, и это — бизнес-код. Откуда он взялся? И не может там быть никакого дополнительного метода.

Берем и прикручиваем к нашему классу какому-то еще одну функцию, которую можем запускать на объектах этого класса.
Если мы передадим этой функции некоторую лямбду, а затем запустим ее на существующем свойстве, то все отлично — метод availability в своей реализации инициализирует свойство availability. Этот метод появляется благодаря extension-функциям. Мы уже знаем про оператор invoke, который может и крепиться к типу, и быть одновременно extension-функцией. От этого можно избавиться. В результате, когда мы работаем с преподавателем, доступность – свойство преподавателя, а не какой-то дополнительный метод, и тут никакого рассинхрона не происходит. Если в этот оператор передавать лямбду, то тут же, на ключевом слове this, мы можем эту лямбду запускать.

Это хорошо, так как если будет переменная с nullable типом, содержащим значение null, наша функция к этому уже готова, и не упадет с NullPointer. В качестве бонуса, extension-функции можно создавать для nullable типов. Внутри этой функции this может быть равен null, и это нужно обработать.

Extension-функция определяется по типу переменной, а не по фактическому типу. Резюмируя по extension-функциям: необходимо понимать, что имеется доступ только к публичному API класса, а сам класс никак не модифицируется. Можно создавать extension-функцию для одного класса, но написать ее в совершенно другом классе, и внутри этой extension-функции будет доступ к одновременно двум контекстам. Более того, член класса с той же сигнатурой окажется приоритетней. Ну и наконец, это отличная возможность взять и прикрутить операторы вообще в любое место, где мы хотим. Получается пересечение контекстов.

Очередной опасный молоток в руках разработчика. Следующий инструмент — инфиксные функции. То, что вы видите – это код. Почему опасный? Пожалуйста, не делайте так. Такой код можно написать в Kotlin, и не надо так делать! Благодаря этому есть возможность избавляться от точек, скобочек — от всего того шумного синтаксиса, от которого мы пытаемся уйти как можно дальше и сделать наш код немного чище. Но тем не менее, подход хороший.

Давайте возьмем более простой пример — переменную типа integer. Как это работает? Если мы допишем слева от нее модификатор infix – все, этого достаточно. Создадим для нее extension-функцию, назовем ее shouldBeEqual, она что-то будет делать, но это уже неинтересно. Можно избавляться от точек и скобочек, но есть парочка нюансов.

На основе этого реализована как раз конструкция data и assertions, скрепленные вместе.

У нас есть SchedulingContext — общий контекст запуска планирования. Давайте в ней разберемся. При этом мы создаем extension-функцию и одновременно инфикс-функцию assertions, которая будет запускать лямбду, проверяющую наши значения. Есть функция data, которая возвращает результат этого планирования.

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

Однако, у нас обязательно должен существовать субъект и объект этого действия, и нужно воспользоваться модификатором infix. Говоря про инфикс функции, важно понимать, что это шаг по избавлению от шумного синтаксиса. Можно передавать в эту функцию, например, лямбды, и таким образом получаются конструкции, которые вы раньше не видели. Может быть точно один параметр — то есть ноль параметров не может быть, два не может быть, три – ну вы поняли.

Ее лучше смотреть на видео, а не читать текстом. Перейдем к следующей демке.

Теперь все выглядит готовым: инфикс функции вы увидели, extension функции увидели, destructuring declaration готов.

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

Наш пользователь (возможно, неопытный пользователь), пишет data внутри data, и это не имеет никакого смысла. Бывают ситуации, когда мы можем взять DSL и переиспользовать его прям внутри него же, а мы этого делать не хотим. Нам хотелось бы как-то запретить ему это делать.

1 мы должны были сделать следующее: в ответ на то, что у нас в SchedulingContext есть метод data, мы должны были в DataContext создать еще один метод data, в который принимаем лямбду (пускай без реализации), должны были пометить этот метод аннотацией @Deprecated и сказать компилятору не компилировать такое. До Kotlin версии 1. Используя такой подход, мы получим даже некоторое осмысленное сообщение, когда будем писать неосмысленный код. Видишь, что такой метод запускается – не компилируй.

1, появилась замечательная аннотация @DslMarker. После версии Kotlin 1. Ими, в свою очередь, мы будем размечать проблемно-ориентированные языки. Эта аннотация нужна, чтобы помечать производные аннотации. Больше нет потребности в том, чтобы писать дополнительные методы, которые нужно запрещать компилировать — оно все просто работает. Для каждого проблемно-ориентированного языка вы можете создать одну аннотацию, которую пометите @DslMarker и будете её вешать на каждый контекст, который необходим. Не компилируется.

Обычно она написана на Java. Тем не менее, есть один такой специальный случай, когда мы работаем с нашей бизнес-моделью. Как думаете, какой контекст внутри метода студент? Есть контекст,  есть аннотация, которой нужно пометить контекст. Это – кусок нашей бизнес-модели, там Kotlin нет. Класс Student.

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

Варианта у нас есть три.

  1. Создать целый контекст, который отвечает за нашего студента. Назовем его StudentContext. Опишем там все свойства, и потом будем на основе него создавать студента. Некоторое такое безумие – пишется куча кода, наверное, больше, чем для продакшена.
  2. Второй вариант – можем взять и создать некоторый интерфейс, который отражает нашего студента, то есть просто перечисляет свойства. Но переиспользуем этот же интерфейс в наших тестах. Возьмем StudentContext и скажем, что он реализует некоторый интерфейс IStudent посредством делегирования реализации этого интерфейса другому объекту. То есть, создается тут же на месте объект Student, и от него берется вся реализация интерфейса IStudent для StudentContext. Помечаем аннотацией DslMarker и прекрасно, все работает.
  3. Любимый способ: воспользуемся аннотацией deprecated и запретим компилировать неправильный код. Просто перечислим то, что нам необходимо. Обычно в иерархии сущностей находится такая сущность, которая содержит идентификатор. На эту сущность мы можем повесить extension-функцию, которую мы и запретим вызывать. В том числе и студента внутри студента.

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

Защищайте ваших пользователей от ошибок. Резюмируя про контроль контекста. Тем более, что реализация такого контроля занимает не так много средств и времени. Понятно, что некоторые ошибки пользователи делать не будут, ведь это очевидно, но контролировать это все равно желательно. В тех ситуациях, когда вы не можете пользоваться аннотацией @DslMarker, воспользуйтесь аннотацией @Deprecated, это поможет вам обойти те случаи, которые пока не работают. Пользуйтесь аннотацией @DslMarker, которой вы помечаете ваши собственные аннотации.

Итак, демка контроля контекста:

Минусы и проблемы

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

Как это сделать? Представим, что у вас есть какой-то кусочек кода, и вы хотите его просто повторять, например, в цикле иметь возможность создавать студентов, много-много раз одинаковых студентов, или любые другие сущности. Можно создать дополнительный метод внутри вашего DSL, и это будет уже более хорошим решением, однако, решать такие проблемы придется прямо на уровне DSL. Можно воспользоваться циклом for — не самый лучший вариант. К счастью, с версии Kotlin плагина 1. Следите за ключевым словом this и дефолтным именованием параметра it. 20 у нас есть хинты, которые видны прямо в среде разработки. 2. Серенький код нам подсказывает, с каким контекстом мы работаем или что такое it.

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

На мой субъективный взгляд, лучшая документация для вашего проблемно-ориентированного языка – это больше количество примеров этого DSL. Наконец, документация. Однако, если пользователь DSL понятия не имеет, какие конструкции имеются, ему и Kotlin-доки смотреть негде. Здорово, когда у вас есть Kotlin-доки, это хороший бонус. Когда вы приходите писать Gradle-файл, в самом начале, вы не понимаете, что в нем есть, и нужны какие-то примеры. Чувствовали такое когда-нибудь? Вам наплевать на какие-то контексты, вы хотите примеры, и вот это – та самая лучшая документация, которой можно пользоваться новым юзерам вашего DSL.

Это очень хочется делать, когда вы владеете этим инструментом. Не суйте DSL’и во все щели, пожалуйста. Во-первых – это неблагодарная работа. Хочется сказать, давайте создадим DSL сюда, может быть, сюда и сюда. Там, где вам это действительно помогает решать какую-то проблему.
Наконец, изучайте Kotlin. Во-вторых, все-таки желательно применять это по месту назначения. И когда вы будете снова возвращаться к тестированию (например, что-то дописали, на это нужно сделать тест), вам будет намного приятнее это делать, потому что DSL максимально компактный, комфортный, и у вас нет проблем с тем, чтобы создать с десяток студентов. Изучайте возможности, которые приходят в этот язык, новые функции, благодаря чему ваш код будет все чище, короче, компактнее, читать его будет намного проще. Просто в пару строчек это делается.

На мой взгляд, сначала проще привнести в ваш проект Kotlin в качестве тестирования. Тренируйтесь на «кошках», как герой одного известного фильма. Это такое поле боя, на котором даже если ничего не получится — ничего страшного, все еще можно этим пользоваться.
Наконец, предварительно проектируйте DSL. Это хорошая возможность проверить язык, попробовать его, посмотреть на его фичи. Если заранее спроектировать DSL, в конечном итоге будет намного проще, вы не будете по 10 раз переделывать его, вы не будете париться о том, что контексты каким-то образом пересекаются и логически сильно связаны. Сегодня я показал некоторый идеальный пример, и мы прошли поэтапно до построения проблемно-ориентированного языка. Просто предварительно спроектируйте DSL – это довольно легко сделать на бумажке, когда вы знаете набор конструкций, которые я вам сегодня рассказал.

Меня зовут Иван Осипов, Telegram: @ivan_osipov, Twitter: @_osipov_, Хабр: i_osipov. И наконец, контакты для связи. Буду ждать ваших комментариев.

Если вам понравился этот доклад с конференции JPoint — обратите внимание, что 19-20 октября в Санкт-Петербурге пройдет Joker 2018 — крупнейшая в России Java-конференция. Минутка рекламы. Конференция анонсирована совсем недавно, но на сайте уже есть первые спикеры и доклады. В его программе тоже будет много интересного.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Перевод] .NET Core + Docker на Raspberry Pi. А это законно?

Открытая платформа .NET Core работает практически на всем: Windows, Mac и десятке Linux-систем. Но еще есть SDK и Runtime. Раньше .NET Core SDK не поддерживался на чипах ARMv7/ARMv8, на которых работает Raspberry Pi. Но все изменилось. Подробнее о способах запуска ...

Финтех-дайджест: в магазине можно будет снять деньги с карты на кассе; PayPal хочет покупать больше компаний

Сегодня в дайджесте: В магазине можно будет снять деньги со своей карты; PayPal собирается тратить около $3 млрд в год на слияния и поглощения; «Альфа-Банк» тестирует международную блокчейн-платформу; Как банки будут собирать биометрические данные? Итак, в этом году отдельные банки ...