Обязательные и один из нескольких идиом
Поддержка Kotlin DSL отличная, но я столкнулся с двумя сценариями, я могу только добавить обходной путь. Оба обходных пути имеют свой главный недостаток, поскольку они навязывают ограничения только во время выполнения.
Первое ограничение: обязательный параметр
Я хотел бы написать что-то вроде этого:
start {
position {
random {
rect(49, 46, 49, 47)
rect(50, 47, 51, 48)
point(51, 49)
}
}
}
где позиция является обязательным параметром. Мой подход состоит в том, чтобы установить нулевое положение при запуске и проверить его при создании начального объекта.
Второе ограничение: одно из многих
Я хотел бы разрешить ровно один из нескольких возможных подобъектов:
start {
position {
random {
[parameters of random assign]
}
}
}
or
start {
position {
user {
[parameters of user assign]
}
}
}
У меня есть ощущение, что я достиг предела возможностей инструментария Kotlin DSL, потому что эти требования также проверяются только на время компиляции на базовом языке.
Любая идея?
2 ответа
Подумав над проблемой, я понял, что эти два требования не могут быть решены в самом Kotlin, поэтому в текущей форме, представленной выше, невозможно получить чисто синтаксическое решение. Однако есть несколько опций, которые могут создать достаточно близкий синтаксис и решить одну или обе проблемы одновременно.
Вариант 1: параметры
Это решение довольно простое и уродливое, добавляя ужасную аномалию "где есть закрывающая скобка". Он просто перемещает свойство position в конструктор:
start(random {
rect(49, 46, 49, 47)
rect(50, 47, 51, 48)
point(51, 49)
}) {
windDirection to NORTH
boat turn (BEAM_REACH at STARBOARD)
}
Это просто в коде:
fun start(pos : StartPosition, op: StartConfigBuilder.() -> Unit) : StartConfigBuilder
= StartConfigBuilder(pos).apply(op)
и создает функции построителя верхнего уровня для реализации позиции:
fun random( op : RandomStartPositionBuilder.() -> Unit) = RandomStartPositionBuilder().apply(op).build()
class RandomStartPositionBuilder {
private val startZoneAreas = mutableListOf<Area>()
fun rect(startRow: Int, startColumn: Int, endRow: Int = startRow, endColumn: Int) =
startZoneAreas.add(Area(startRow, startColumn, endRow, endColumn))
fun point(row: Int, column: Int) = startZoneAreas.add(Area(row, column))
fun build() = RandomStartPosition(if (startZoneAreas.isEmpty()) null else Zone(startZoneAreas))
}
fun user( op : UserStartPositionBuilder.() -> Unit) = UserStartPositionBuilder().apply(op).build()
class UserStartPositionBuilder {
fun build() = UserStartPosition()
}
Хотя это решает как необходимые, так и только одни проблемы во время редактирования, значительно затрудняет чтение DSL и мы теряем элегантность инструментов DSL. Это станет еще более беспорядочным, если в конструктор будет перенесено более одного свойства или внутренний объект (позиция) станет более сложным.
Вариант 2: инфиксная функция
Это решение перемещает необходимое сложное поле за пределы блока (это "неприятная" часть) и использует его как инфиксную функцию:
start {
windDirection to NORTH
boat turn (BEAM_REACH at STARBOARD)
} position random {
rect(49, 46, 49, 47)
rect(50, 47, 51, 48)
point(51, 49)
}
or
start {
windDirection to NORTH
boat turn (BEAM_REACH at STARBOARD)
} position user {
}
Это решение решает проблему "только одна", но не "точно одна".
Чтобы добиться этого, я модифицировал сборщиков:
//Note, that the return value is the builder: at the end, we should call build() later progmatically
fun start(op: StartConfigBuilder.() -> Unit) : StartConfigBuilder = StartConfigBuilder().apply(op)
class StartConfigBuilder {
private var position: StartPosition = DEFAULT_START_POSITION
private var windDirectionVal: InitialWindDirection = RandomInitialWindDirection()
val windDirection = InitialWindDirectionBuilder()
val boat = InitialHeadingBuilder()
infix fun position(pos : StartPosition) : StartConfigBuilder {
position = pos
return this
}
fun build() = StartConfig(position, windDirection.value, boat.get())
}
// I have to move the factory function top level
fun random( op : RandomStartPositionBuilder.() -> Unit) = RandomStartPositionBuilder().apply(op).build()
class RandomStartPositionBuilder {
private val startZoneAreas = mutableListOf<Area>()
fun rect(startRow: Int, startColumn: Int, endRow: Int = startRow, endColumn: Int) =
startZoneAreas.add(Area(startRow, startColumn, endRow, endColumn))
fun point(row: Int, column: Int) = startZoneAreas.add(Area(row, column))
fun build() = RandomStartPosition(if (startZoneAreas.isEmpty()) null else Zone(startZoneAreas))
}
// Another implementation
fun user( op : UserStartPositionBuilder.() -> Unit) = UserStartPositionBuilder().apply(op).build()
class UserStartPositionBuilder {
fun build() = UserStartPosition()
}
Это решает проблему реализации "только один" почти элегантным способом, но не дает ответа на опцию "обязательное свойство". Так что это хорошо, когда может быть применено значение по умолчанию, но все же дает только исключение времени разбора, когда позиция отсутствует.
Варианты 3: цепочка инфиксных функций
Это решение является вариантом предыдущего. Для решения требуемой проблемы предыдущего мы используем переменную и промежуточный класс:
var start : StartWithPos? = null
class StartWithoutPos {
val windDirection = InitialWindDirectionBuilder()
val boat = InitialHeadingBuilder()
}
class StartWithPos(val startWithoutPos: StartWithoutPos, pos: StartPosition) {
}
fun start( op: StartWithoutPos.() -> Unit): StartWithoutPos {
val res = StartWithoutPos().apply(op)
return res
}
infix fun StartWithoutPos.position( pos: StartPosition): StartWithPos {
return StartWithPos(this, pos)
}
Тогда мы могли бы написать следующее утверждение в DSL:
start = start {
windDirection to NORTH
boat heading NORTH
} position random {
}
Это решило бы обе проблемы, но со стоимостью дополнительного назначения переменных.
Все три решения работают, добавляет немного грязи в DSL, но можно было бы выбрать, которое подходит лучше.
Вы можете черпать вдохновение из собственного HTML DSL Kotlin. Для обязательных аргументов используйте простые функции с аргументами, а не функцию литерал с получателем.
Ваш DSL будет выглядеть примерно так:
start(
position {// This is mandatory
random {// This is not
}
}
)
И ваш start
строитель:
fun start(position: Position): Start {
val start = Start(position)
...
return start
}
Используйте тот же подход для position()
,