Маленький dsl

от автора

Введение

Волею случая и некоторых немалых усилий ушел со старого ужасного места работы и джавы и пришел на новое прекрасное(пока) с котлин. Очень сахарный язык. А все мы любим сладенькое. Очень понравилась идея котлин дсл и захотелось самому освоить и написать что-то простенькое.

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

Что мы сегодня напишем Брейн?

Все будет просто. Будем обрабатывать текстовые команды в зависимости от состояния, по-разному. Пусть нам приходит id по которому будем доставать состояние и команда. Ну и немного котлиновского сахара, в целом это не обязательно, но так выглядит красивше. Дженерик в этом классе — перечисление всех возможных состояний.

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)     } }

StateService — это интерфейс и его реализация не особо интересна, да и StateProviderService довольно прост. Map состояний против обработчиков команд, а в нем Map команд против действия.

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")     } }

Проблема теперь — заполнить все эти мапы.

Да будет билдер

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

В котлине можно вызывать функцию apply() она дает возможность заполнить все что нужно. Проблема в том, что прийдется открыть все поля и сделать пустой конструктор. Тогда бы это выглядело примерно так:

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     } }

В котлине если лямбда — последний аргумент, ее можно вынести за скобки. Например функция state:

state(TestState.A) {       action("go-to-B") {             println("i'm mooving to B")             TestState.B       } }

Вааааа как красиво. Теперь просто соберем Processor и плов готов.

fun <E : Enum<E>> processor(stateService: StateService<E>, builder: StateProviderBuilder<E>.() -> Unit): Processor<E> =     Processor(StateProviderBuilder<E>().apply(builder).toService(), stateService)

Во первых это красиво

Теперь добавим чутка сахара. Есть такая штука infix функции, они могут использоваться как операторы. Допустим теперь я хочу писать «print» doing {…}

infix fun String.doing(action: () -> E) {     action(this, action) }

Ну и появилась одна заковыка, допустим я могу перейти из разных состояний одним способом. Для этого нам нужно собрать состояния и действие и передать их дальше. Как-то так это listOf(TestState.A, TestState.B) withAction «take the world» doing

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://habr.com/ru/post/704168/


Комментарии

Добавить комментарий

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