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

Kotlin - extensible type-safe builders

我希望能够创建自定义构建器模式 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...
    }
}

在这里,所有可能的 "elements" 都被预定义并实现为函数,也 return 相应类型的对象。 (例如 html 函数 return 是 HTML class 的实例)

每个函数都被定义为将自己作为子对象添加到其父上下文的对象中。

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

class NewElem : Element() {
    // ...
}

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

每次。

是否有一种干净的方法来隐藏此实现细节?

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

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

我尝试过的可能性

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

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

// 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" }
    }
}

不幸的是,无法强制 "static" 函数以类型安全的方式由 subclasses 实现。

此外,伴生对象不会被继承,因此对子classes 的调用无论如何都不会起作用。

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

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 是针对 "invoke" 调用并尝试将其传递给构造函数。

这可以通过稍微减少一些清洁来解决:

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 子类添加某种辅助函数是不可避免的,但是可以使用通用辅助函数简化它们的实现。


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

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)

这些仍然让您不得不使用适当的 header 声明一个工厂函数,但这是实现 HTML 示例实现的语法的唯一方法,其中 NewElem 可以用它自己的 newElem 函数创建。

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

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

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

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()
        }