mylist.first();
/* there isn’t first() method
in mylist collection*/
Ivan Ponomarev
Иван Пономарёв
|
Рассуждения о сильных и слабых сторонах Kotlin DSL по сравнению с другими вариантами создания DSL-ей
Несколько примеров задач, решаемых с помощью Kotlin DSL
Некоторые продвинутые частности проектирования Kotlin DSL
Введения в создание Kotlin DSL (но будет много ссылок на соответствующие материалы)
Martin Fowler, Rebecca Parsons |
|
|
|
|
* НЕ означает, что вы в полной безопасности, см. напр. "Billion_laughs_attack" или доклады Сергея Васильева на Heisenbug и Joker про уязвимости XML-парсинга.
Kotlin DSLs относятся к классу внутренних DSL на основе статических языков программирования
У этого есть как преимущества, так и недостатки
Выбор за вами!
Дмитрий Жемеров, Светлана Исакова |
Иван Осипов Kotlin DSL: from Theory to Practice
https://www.jmix.io/cuba-blog/kotlin-dsl-from-theory-to-practice/
JPoint 2018/Heisenbug 2018:
https://www.youtube.com/watch?v=q_UM1EY2S5g
Anton Arhipov
Kotlin DSL in under an hour
https://www.youtube.com/watch?v=0DJqr4FZ6f0
Tool | DSL syntax | General syntax |
Extension functions |
|
|
Infix functions |
|
|
Operators overloading |
|
|
Type aliases |
| Creating empty inheritors classes and other duct tapes |
Tool | DSL syntax | General syntax |
get/set methods convention |
|
|
Destructuring declaration |
|
|
Lambda out of parentheses |
|
|
Lambda with receiver (главный инструмент) |
| N/A |
Context control |
| N/A |
Наша предметная область:
Условия
Трансформации
Правила
Если выполняется набор условий — запускается нужная трансформация
fun main() {
if (conditionOneMet() && conditionTwoMet()) {
runTransformationA()
} else if ((ConditionIII.met() || ConditionIV.met()) && conditionOneMet()) {
runTransformationB()
} else if (conditionTwoMet()) {
runTransformationC()
}
}
пошаговая отладка!
тестирование с оглядкой на coverage (хотя подмена conditionXmet()
и runTransformationX()
моками может представлять трудность)
код разрастается и быстро становится трудно читаемым/поддерживаемым
private fun rules(): List<Rule> = listOf(
Rule(ConditionII, TransformationC),
Rule(Not(ConditionIV), TransformationB),
Rule(And(ConditionI, ConditionII), TransformationA),
Rule(Or(And(ConditionIII, ConditionIV), ConditionI), TransformationB)
)
fun main() {
rules()
.firstOrNull { it.condition.met() }
?.transformation?.run()
}
Такое можно написать и на Java, но на Kotlin получилось компактнее из-за отсутствия new
.
Визуально связь правил и трансформаций лучше воспринимается
Пошаговая отладка превратилась в ад
val rules: List<Rule> =
// @formatter:off
rules {
ConditionI and ConditionIV invokes TransformationA
ConditionII invokes TransformationC
not(ConditionIV) invokes TransformationB
(ConditionI and not(ConditionIII)) invokes TransformationA
(ConditionIII
and ConditionIV
or ConditionI) invokes TransformationB
}
// @formatter:on
Параметр метода rules
— лямбда с ресивером
and
, or
, not
, invokes
— инфиксные функции-расширения
fun main() {
rules.firstOrNull { it.condition.met() }?.transformation?.run()
}
Достоинство: весь код в одну строчку
Недостаток: пошаговая отладка правил невозможна (но это можно компенсировать, см. далее)
for (rule in rules) {
rule.visit(::visitor)
}
class Rule(...) : Element {
override fun visit(visitor: (Element) -> Unit) {
condition.visit(visitor)
transformation.visit(visitor)
visitor.invoke(this)
}
}
"Почти бесплатное" представление DSL в виде JSON/YAML/XML средствами, например, FasterXML Jackson.
Идеально для построения WebUI с формами для показа/редактирования настроек (projectional editors)
- condition:
And:
a:
ConditionI: {}
b:
ConditionIV: {}
transformation:
TransformationA: {}
Код, порождающий 2N комбинаций множеств выполняющихся условий,
где N — число субклассов BasicCondition
private val conditions = BasicCondition::class.sealedSubclasses
fun outcomes(): Sequence<Set<ConditionClass>> = sequence {
for (i in 0L until (1L shl conditions.size)) { // тут возникает 2^N
val activeConditions = mutableSetOf<ConditionClass>()
for (j in 0 until conditions.size) {
if ((i and (1L shl j)) != 0L) {
activeConditions.add(conditions[j])
}
}
yield(activeConditions)
}
}
"Не существует недостижимых правил"
"При фиксированном условии, каждое из правил определенного класса достижимо"
… и т. д. — всё зависит от вашей задачи
Тестируем саму модель, заданную в DSL, а не результат её интерпретации!
Выполнения правил
Генерации документации
Визуализации
Валидации
Сериализации ("бесплатная" JSON/YAML/XML-версия нашего Kotlin DSL)
Groovy Gradle DSL:
implementation 'com.acme:example:1.0'
Kotlin Gradle DSL:
implementation ("com.acme:example:1.0")
this
val jpoint = javaConference {
//Без круглых скобок нельзя (в отличие от Groovy)
talk("Пишем приложение на Ktor") deliveredBy {
speaker("Александр Нозик")
speaker("Глеб Королькевич")
}
talk("One source to rule them all: Kotlin DSL") deliveredBy {
speaker("Иван Пономарев")
} withExperts {
speaker("Андрей Кулешов")
}
}
this
: val jpoint = javaConference {
//Всё без скобок (но и без осмысленного имени метода)
+ "Пишем приложение на Ktor" deliveredBy {
+ "Александр Нозик"
+ "Глеб Королькевич"
}
+ "One source to rule them all: Kotlin DSL") deliveredBy {
+ "Иван Пономарев"
} withExperts {
+ "Андрей Кулешов"
}
}
По смыслу стуктуры DSL нам бы такого не хотелось, но лямбда с ресивером это не запрещает:
javaConference { //this: ConferenceBuilder
talk ("Talk 1") deliveredBy {
//this: ConferenceBuilder.SpeakersBuilder,
//но также доступны методы из ConferenceBuilder
talk (...) // ???!!!
}
}
@DslMarker
annotation class MeetupDsl
@MeetupDsl
class MeetupBuilder { ... }
@MeetupDsl
class SpeakersBuilder { ... }
Также обещана (но не задокументирована) расширенная поддержка со стороны IDE, поэтому имеет смысл размечать DSL-биледеры с помощью @DslMarker
в любом случае.
private val jpoint = javaConference {
val ip = Speaker("Иван Пономарев", "N/A")
talk("One source to rule them all: Kotlin DSL") deliveredBy {
+ ip
} withExperts {
+ Speaker("Андрей Кулешов", "Huawei")
}
talk("Kotlin Script: для кого, зачем и как") deliveredBy {
+ Speaker("Анатолий Нечай-Гумен", "Банк «Центр-инвест»")
} withExperts {
+ ip
}
}
val ip = …
выглядит как императивный код, но ничего тут поделать нельзя
В Groovy тут гораздо больше возможностей
//ConferenceBuilder
val speakers = mutableMapOf<String, Speaker>()
//Функция возвращает делегат свойства только для чтения
fun speaker(name: String, company: String): ReadOnlyProperty<Nothing?, Speaker> =
ReadOnlyProperty { _, property ->
//property.name содержит имя переменной
speakers.computeIfAbsent(property.name) { Speaker(it, name, company) }
}
//Сам язык будет гарантировать нам уникальность идентификаторов
val an by speaker("Александр Нозик", "МФТИ")
val gk by speaker("Глеб Королькевич", "Хоум Банк")
talk("Пишем приложение на Ktor") deliveredBy {
+an
+gk
}
Gradle Kotlin DSL
Ktor Framework: https://ktor.io/ (а кстати на этом JPoint есть воркшоп на тему Ktor!)
Exposed (an ORM for Kotlin): https://github.com/JetBrains/Exposed?tab=readme-ov-file#examples
DSL в сочетании с дизайн-паттернами представляет собой мощный инструмент для решения множества задач.
Создавать DSL в Kotlin не страшно. Прямо сегодня вы можете улучшить части существующих внутренних API, сделав их «DSL-подобными».
Внутренние DSL Kotlin — не единственный способ реализации DSL, со своими сильными и слабыми сторонами, но определенно не самый худший во многих сценариях.
Код и слайды доступны GitHub https://github.com/inponomarev/dsl-talk | @inponomarev |