Введение
Пересел недавно с джавы на котлин и начал осваивать новые сахарные возможности. На проекте у нас во многих местах используются дсл. Я люблю подобные штуки и решил освоить, как и из чего это состоит.
Дано.
- На вход программе приходят текстовые команды и идентификатор пользователя.
- Пользователь может находиться в разных состояниях.
- В зависимости от состояния есть разные наборы команд которые возможно обработать.
- Если команда не соответствует ни одной из возможных, выполняется команда по умолчанию.
Что мы сегодня напишем Брейн?
![](https://habrastorage.org/getpro/habr/upload_files/f6b/925/1b0/f6b9251b0d5afb070332f6b937e2ccce.jpeg)
Для начала нужно написать саму систему, которая будет выполнять требуемые действия. Она довольно простая. У нас есть Processor, который достает состояние пользователя и передает StateProviderService команду и состояние. В ответ он получает набор возможных команд в новом состоянии.
class Processor<E : Enum<E>>(
private val stateProviderService: StateProviderService<E>,
private val stateService: StateService<E>
) {
fun process(action: String, identifier: Any): Set<String> {
return stateService.getState(identifier)
.provide(action)
.nextState(identifier)
.getActions()
}
private fun E.provide(action: String): E {
return stateProviderService.provide(this, action)
}
private fun E.nextState(identifier: Any): E {
return stateService.nextState(this, identifier)
}
private fun E.getActions(): Set<String> {
return stateProviderService.getActions(this)
}
}
StateProviderService и ActionProviderService устроены очень похоже. В зависимости от состояния StateProviderService вызывает обработку пришедшей команды у определенного обработчика. Аналогично ActionProviderService с командами.
class StateProviderService<E : Enum<E>> (
private val stateProviders: Map<E, ActionProviderService<E>>
) {
fun provide(state: E, action: String): E {
return stateProviders[state] ?.provide(action) ?: throw Exception("StateProvider must provide all states")
}
fun getActions(state: E): Set<String> {
return stateProviders[state]?.actionProviders?.keys ?: throw IllegalArgumentException("State $state has no actions")
}
}
class ActionProviderService<E : Enum<E>>(
val actionProviders: Map<String, () -> E>,
val defaultProvider: (() -> E)?
) {
fun provide(action: String): E {
return actionProviders[action] ?.invoke() ?: defaultProvider?.invoke() ?: throw Exception("ActionProvider must provide all actions")
}
}
Да будет билдер
![](https://habrastorage.org/getpro/habr/upload_files/d1a/493/f6a/d1a493f6adb1b064b6c07a492c349052.jpeg)
Следующая задача - заполнить все обработчики. Если бы это был спринг, я бы написал свою аннотацию и помечал бы ей классы обработчики и с помощью бпп заполнял мапы. На этот раз я хочу решить эту проблему с помощью дсл.
В котлине можно вызывать функцию apply(). Она дает доступ к содержимому объекта и с помощью нее можно удобно изменять его состояние. Проблема в том, что у такого объекта прийдется открыть все поля и сделать пустой конструктор. В случае Processor-а это выглядело бы так:
Processor().apply {
stateProviderService = StateProviderService(...)
stateService = StateServiceImpl()
}
Я не хочу изменять таким образом системные сервисы. Напишем билдер, который можно будет таким образом настраивать. В билдере, помимо функции получения объекта сервиса, добавим вспомогательные функции для его настройки.
class StateProviderBuilder<E : Enum<E>> {
private val stateProviders: MutableMap<E, ActionProviderService<E>> = mutableMapOf()
fun toService() = StateProviderService(stateProviders)
fun state(state: E, builder: ActionProviderBuilder<E>.() -> Unit) =
stateProviders.put(state, ActionProviderBuilder<E>().apply(builder).toService())
}
class ActionProviderBuilder<E : Enum<E>> {
private val actionProviders: MutableMap<String, () -> E> = mutableMapOf()
private var defaultProvider: (() -> E)? = null
fun toService() = ActionProviderService(actionProviders, defaultProvider)
fun action(action: String, body: () -> E) = actionProviders.put(action, body)
fun default(body: () -> E) {
defaultProvider = body
}
}
В некоторые функции мы передаем билдер и странно выглядящую лямбду. Такая лямбда как например ActionProviderBuilder.() -> Unit позволяет получить доступ к объекту ActionProviderBuilder, как если бы она передавалась в функцию apply. Таким образом вызвав функцию state мы получаем доступ к функциям toService, action, default у объекта класса ActionProviderBuilder внутри лямбды.
Еще одна приятная особенность - в котлине если лямбда - последний аргумент, ее можно вынести за скобки. В итоге вызов функции state выглядит так:
state(TestState.A) {
action("go-to-B") {
println("i'm mooving to B")
TestState.B
}
}
Во первых это красиво
В целом результат меня уже устроил, но мне хотелось попробовать infix функции. Это такие функции, которые могут использоваться как операторы. Например я могу сделать функцию, чтобы писать "print" doing {...}. И на самом деле, для этого всего лишь надо дописать рядом с функцией action следующее:
infix fun String.doing(action: () -> E) {
action(this, action)
}
И под конец у меня появилась идея. Допустим есть одинаковые переходы из нескольких состояний в другое. Повторять несколько раз одно и то же не хотелось бы, так что допишу еще вот такой вариант: listOf(TestState.A, TestState.B) withAction "action" doing {...}. Здесь я хочу для каждого из перечисленных состояний добавить выполнение некоторой логики по команде "action".
infix fun List<E>.withAction(actionName: String) =
ActionContainer(listOf(this), listOf(actionName))
infix fun ActionContainer<E>.doing(action: () -> E) =
addActionWithActionContainer(this, action)
private fun addActionWithActionContainer(actionContainer: ActionContainer<E>, action: () -> E) {
actionContainer.states.forEach {
it state { actionContainer.actionNames.doing(action) }
}
}
class ActionContainer<E : Enum<E>>(
val states: List<E>,
val actionNames: String
)
Чему мы научились Палмер
![](https://habrastorage.org/getpro/habr/upload_files/f79/b5f/4ed/f79b5f4ed90e7bebc264ccda877bf47c.jpeg)
Kotlin dsl великолепная вещь! После джавы я получаю огромное удовольствие от таких новых возможностей. Я не коснулся переопределения операторов в этой статье, но такое тоже возможно.
Пы Сы
Это переписанная версия, извиняюсь перед всеми, кто видел предыдущую. Очень хотелось быстрее написать, и видимо делать это в ночь - не самая удачная затея.
Вот репозитрий
Вот телеграмм (я там пропал надолго, но уже вроде как вернулся)
ris58h
Минус ставить не стал, но "Ничего не понял после прочтения".
Какие-то провайдеры, сервисы, билдеры - как в Java EE окунулся. За этим сути не видно. Что сделать хотим и как этим пользоваться? Начать стоит с проблемы и примера, а не с "кишок".
Andrey210 Автор
Не могу отрицать. Очень хотелось быстрее опубликовать и писал в ночь, а седня прочитал и ужаснулся. Попытался переписать если у васч будут исправления с удовольствием выслушаю и поправлю.