Подумав о проблеме, я понял, что эти два требования не могут быть решены в самом 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()
}
Это решает проблему реализации "only-one" почти элегантным способом, но не дает ответа на параметр "required property". Поэтому хорошо, когда может быть применено значение по умолчанию, но все же выдает только исключение времени разбора, когда позиция отсутствует.
Опции 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, но можно было бы выбрать, которое подходит лучше.