Kotlin - 可扩展的类型安全构建器



我希望能够创建自定义构建器模式DSL类型的东西,并且我希望能够以干净和类型安全的方式创建新组件。如何隐藏创建和扩展此类构建器模式所需的实现详细信息?

Kotlin 文档给出了类似于以下示例的内容:

html {
head {
title {+"XML encoding with Kotlin"}
}
body {
h1 {+"XML encoding with Kotlin"}
p  {+"this format can be used as an alternative markup to XML"}
a(href = "http://kotlinlang.org") {+"Kotlin"}
// etc...
}
}

在这里,所有可能的"元素"都是预定义的,并作为函数实现,这些函数也返回相应类型的对象。(例如,html函数返回HTML类的实例(

定义每个函数,以便将自身作为子级添加到其父上下文的对象中。

假设有人想创建一个可用作newelemNewElem的新元素类型。他们将不得不做一些繁琐的事情,例如:

class NewElem : Element() {
// ...
}
fun Element.newelem(fn: NewElem.() -> Unit = {}): NewElem {
val e = NewElem()
e.fn()
this.addChild(e)
return e
}

每次。

有没有一种干净的方法来隐藏这个实现细节?

例如,我希望能够通过简单地扩展Element来创建新元素。

如果可能的话,我不想使用反射。

我尝试过的可能性

我的主要问题是想出一个干净的解决方案。我想到了其他几种没有成功的方法。

1( 使用函数调用创建新元素,该函数调用返回要在构建器样式中使用的函数,例如:

// Pre-defined
fun createElement(...): (Element.() -> Unit) -> Element
// Created as
val newelem = createElement(...)
// Used as
body {
newelem {
p { +"newelem example" }
}
}

这样做有明显的缺点,我也没有看到实现它的明确方法 - 可能涉及反思。

2( 覆盖配套对象中的调用运算符

abstract class Element {
companion object {
fun operator invoke(build: Element.() -> Unit): Element {
val e = create()
e.build()
return e
}
abstract fun create(): Element
}
}
// And then you could do
class NewElem : Element() {
companion object {
override fun create(): Element {
return NewElem()
}
}
}
Body {
NewElem {
P { text = "NewElem example" }
}
}

不幸的是,不可能以类型安全的方式强制子类实现"静态"函数。

此外,伴随对象不是继承的,因此对子类的调用无论如何都不起作用。

我们再次遇到将子元素添加到正确上下文的问题,因此构建器实际上并没有构建任何东西。

3( 覆盖元素类型的调用运算符

abstract class Element {
operator fun invoke(build: Element.() -> Unit): Element {
this.build()
return this
}
}
class NewElem(val color: Int = 0) : Element()
Body() {
NewElem(color = 0xff0000) {
P("NewElem example")
}
}

这可能已经奏效,除了当您立即尝试调用由构造函数调用创建的对象时,编译器无法判断 lambda 用于"调用"调用并尝试将其传递到构造函数中。

这可以通过使某些内容稍微不那么干净来修复:

operator fun Element.minus(build: Element.() -> Unit): Element {
this.build()
return this
}
Body() - {
NewElem(color = 0xff0000) - {
P("NewElem example")
}
}

但同样,如果没有反射或类似的东西,实际上不可能将子元素添加到父元素中,因此构建器实际上仍然没有构建任何东西。

4( 调用子元素的add()

为了尝试解决构建器实际上没有构建任何东西的问题,我们可以为子元素实现一个add()函数。

abstract class Element {
fun add(elem: Element) {
this.children.add(elem)
}
}
Body() - {
add(NewElem(color = 0xff0000) - {
add(P("NewElem red example"))
add(P("NewElem red example 2"))
})
add(NewElem(color = 0x0000ff) - {
add(P("NewElem blue example"))
})
}

但这显然是不干净的,只是将繁琐推迟到使用方面而不是实现方面。

我认为为您创建的每个Element子类添加某种帮助程序函数是不可避免的,但是它们的实现可以通过通用帮助程序函数来简化。


例如,您可以创建一个执行安装调用并将新元素添加到父元素的函数,然后您只需调用此函数并创建新元素的实例:

fun <T : Element> Element.nest(elem: T, fn: T.() -> Unit): T {
elem.fn()
this.addChild(elem)
return elem
}
fun Element.newElem(fn: NewElem.() -> Unit = {}): NewElem = nest(NewElem(), fn)

或者,您可以通过反射创建该实例以进一步简化,但由于您已经声明要避免它,这可能看起来没有必要:

inline fun <reified T : Element> Element.createAndNest(fn: T.() -> Unit): T {
val elem = T::class.constructors.first().call()
elem.fn()
this.addChild(elem)
return elem
}
fun Element.newElem(fn: NewElem.() -> Unit = {}) = createAndNest(fn)

这些仍然使您必须使用适当的标头声明工厂函数,但这是实现 HTML 示例实现的语法的唯一方法,其中可以使用自己的newElem函数创建NewElem

我想出了一个不是最优雅的解决方案,但它可以通过并且按照我想要的方式工作。

事实证明,如果您在类重写运算符(或为此创建任何扩展函数(,它就可以访问其父上下文。

所以我覆盖了一元+运算符

abstract class Element {
val children: ArrayList<Element> = ArrayList()
// Create lambda to add children
operator fun minus(build: ElementCollector.() -> Unit): Element {
val collector = ElementCollector()
collector.build()
children.addAll(collector.children)
return this
}
}
class ElementCollector {
val children: ArrayList<Element> = ArrayList()
// Add child with unary + prefix
operator fun Element.unaryPlus(): Element {
this@ElementCollector.children.add(this)
return this
}
}
// For consistency
operator fun Element.unaryPlus() = this

这允许我创建新元素并像这样使用它们:

class Body : Element()
class NewElem : Element()
class Text(val t: String) : Element()
fun test() =
+Body() - {
+NewElem()
+NewElem() - {
+Text("text")
+Text("elements test")
+NewElem() - {
+Text("child of child of child")
}
+Text("it works!")
}
+NewElem()
}

最新更新