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

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

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

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

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

ec3fa3670fd1c8a3cf7222f7e746933c.jpg

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

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


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

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

031be427d58c5073a3bc619cbed65792.jpg

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

f640324b1a2ff556a4da892822847a9c.jpg

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

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

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

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

fda0f594d14b8043054bcae7fe3315ad.jpg

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

521543590a00c6101abd4fcefd345e04.jpg

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

c7fc5711ccf2a4e9dfb626ea64cffb9b.jpg

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

259e9b7a39e84a41466572b38afc280d.jpg

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

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

d55610ef71d368b8d16c1fdef35cd2c5.jpg

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

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


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

e2b7c5f48dbd065e95b1617ad10622ba.jpg

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

Domain Specific Language


f81c790a921e43d645fc7380607cf8ea.jpg

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

2f8e452f62f39f0f56a2c73e355dddcb.jpg

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

07f4cb57f84ba884bd558ee8e97202a2.jpg

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

d1db214b39b5620875350b382622e662.jpg

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

0502e729ebf030ea5e4d9f038a4b7cc6.jpg

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

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

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

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

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


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

5e3695850435a204b9cbb3b94f6f7635.png

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

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

03d699a71ecf86e0e5c03b7a6de57995.png

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

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

a7bdb965641ca434f1eba5e7781b9b53.png

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

3786e6eafdb8ebd14c344af30698df56.png

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

76ba76554585689a4c2562167fd26833.png

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

deba5df0a67a08566e07370bf2f70bc0.png

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

8aa09da05c7c48282e01b0ea409e0f3c.png

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


5f61136ef41733e6fb88c5ff17d6975f.jpg

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

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

Лямбды в Kotlin


val lambda: () -> Unit = { }

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

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

a48f4dd497c8aa6b93ab2043e014ca02.jpg

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

194fab62612fb71e6a5f148193ec1ff9.jpg

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

1ff80ede232393839186b9345065700c.jpg

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

c8dd2b952caa82b67157f41dc79535b0.jpg

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

b80d7c0d814f3c51a91f2c224707a2d5.jpg

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

74b5dda953b1df973a8d893f7a615195.jpg

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

69e4bd781897ef0515384f81367899cc.jpg

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

8bd3755b1085f3049a7ea32c7cc41f79.jpg

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

d80552181518f155fc596c1bb3ddf2aa.jpg

78863db1732d9b92bff62bf82060771a.jpg

770a1b0388cd2380c491cdf5ca8c344c.jpg

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

15bf363d94919f7c73d775c5d28c6c53.jpg

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

e124cce3bd7bdb776c3e946edf8ec52c.jpg

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

eba2868b306f476c35df51d8af500335.jpg

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

d3f954e2a7016ecf31a081559674d01f.jpg

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

3624d83be81ee01223ad758e9e81c94d.jpg

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

cabd4a940deacfcdcb680c4916b29eae.jpg

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

0b457242103fc70de806c9de62234135.jpg

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

b1b819fdb7488092550b2ac72265cd72.jpg

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

50ddbc4920ffb871918ce7c063a874a2.jpg

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

b72155316d99a7e4816d1234644b761b.jpg

Применение


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

83af302ff3c0a85db220c70f0d567743.jpg

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

fcc1acdfc43284500c38884d88212877.jpg

Операторы


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

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

17b3609ab2ebd28cf27f906934536e05.jpg

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

721aea73d36a41cf3dbecd328b1f5ab5.jpg

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

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

d3d2260651fc4049ec8d3e6878a0cc51.jpg

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

3c68c957760612b06a5b008c734ddb33.jpg

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

25f3a1f95c8c61dcb4937822bd9578e6.jpg

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

b6b61d67518cb1dc730bce8e309ae0dc.jpg

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

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

53a41f7d8e9067b6ce850045e745fb4f.jpg

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

22d0e39ef38ceb43fd60654de876691b.jpg

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

1e6895b9ed089ed99d4c2639e6e1f578.jpg

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

8159f5a8628b6c5bb21d219e395ea32b.jpg

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

4c803d2f20ed8b7f79e308d47b734bd9.jpg

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

Демо: object, operators


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

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

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

ukpg0djbwn_hfyq4a7g9usa65aa.png

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

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


v_mqia50010hyrqza7zubejj62e.png

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

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

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

l_3quwu2xaqlxikn71jhpnnero4.png

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

fn1fxa5legi4mwszf5xbudchoi0.png

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

vjbts9wb6fely-atqa-kr88i9qm.png

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

izgddvy3bomaqjp_6yvbfrbcgpe.png

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

b4ff23a77974a299ecac5c1c3b5d984f.jpg

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

f2c1855f1ee98b51a4bda6ce9cc0b0a8.jpg

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

44495f0a381b71a20578736f0cb491af.jpg

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

126db9d789acf9b3482f314d5a53261f.jpg

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

hdnfoutg14046a351gjyhodfprc.png

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

e2b37fa2fad7cd8e201987c32fbf572b.jpg

e59bc7479e66cd96c4d0d5093ef33608.jpg

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

96acaeaa91222d55c4a77b70c9b02903.jpg

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

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

393d7d5d739ea9cecedadff695ca6874.jpg

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

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

521031f59a8e7b222c2614b32ba88abd.jpg

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

54e0446838b2360d695b45e5bacec69d.jpg

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

4d61aff7169c403c75493790720c0e6e.jpg

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

59ae331c58bcd16003f51f870b89549e.jpg

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

a041e86102be21acf2c3170cf941d064.jpg

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

© Habrahabr.ru