经过一番思考,我意识到,这两个要求在 Kotlin 本身中是无法解决的,因此在上面介绍的当前形式中,不可能有纯粹的语法解决方案。但是,有一些选项可能会产生足够接近的语法并同时解决一个或两个问题。
选项 1:参数
这个解决方案非常简单和丑陋,添加了可怕的“where-is-the-closure-parenthesis”异常。它只是将 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 增加了一些污垢,但人们可能会选择更适合的一种。