必选习语和更多习语之一

时间:2019-01-06 13:36:03

标签: kotlin kotlin-dsl

Kotlin DSL支持很棒,但是我遇到了两种情况,我只能添加解决方法。两种解决方法都有其主要缺点,因为它们仅在执行时强制执行约束。

第一个约束:必填参数

我想写这样的东西:

start {
    position {
        random {
            rect(49, 46, 49, 47)
            rect(50, 47, 51, 48)
            point(51, 49)
        }
    }
}

其中position是必需的参数。我的方法是在启动时将位置设置为null,并在构建起始对象时对其进行检查。

第二个约束:众多约束之一

我想精确地允许以下几个子对象之一:

start {
    position {
        random {
            [parameters of random assign]
        }
    }
}

or

start {
    position {
        user {
            [parameters of user assign]
        }
    }
}

我觉得我已经达到了Kotlin DSL工具包的极限,因为此要求也仅在核心语言中经过了编译时验证。

有什么主意吗?

2 个答案:

答案 0 :(得分: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()使用相同的方法。

答案 1 :(得分:0)

考虑了问题后,我意识到,这两个要求无法在Kotlin本身中解决,因此,在上面介绍的当前形式中,不可能有任何纯粹的句法解决方案。但是,有一些选项可能会产生足够接近的语法并同时解决一个或两个问题。

选项1:参数

此解决方案非常简单且难看,添加了可怕的“ where-is-the-thes-thes-closing-括号”异常。它只是将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()
}

这几乎以一种优雅的方式解决了“仅一个”实现的问题,但是没有给出“ 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带来了一些麻烦,但可能会选择一种更合适的方法。