0

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 工具包的可能性边缘,因为这个要求也只在核心语言中进行了编译时验证。

任何想法?

4

2 回答 2

0

您可以从 Kotlin 自己的 HTML DSL 中获得灵感。对于强制参数,使用带有参数的简单函数,而不是带有接收器的函数文字。

你的 DSL 看起来像这样:

start(
    position {// This is mandatory
        random {// This is not

        }
    }
)

和你的start建设者:

fun start(position: Position): Start {
    val start = Start(position)
    ...
    return start
}

对 .使用相同的方法position()

于 2019-01-06T21:47:48.993 回答
0

经过一番思考,我意识到,这两个要求在 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 增加了一些污垢,但人们可能会选择更适合的一种。

于 2019-01-06T21:54:15.567 回答