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

时间:2018-09-08 23:59:07

标签: kotlin factory builder

我希望能够创建自定义的构建器模式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类的实例)

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

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

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)覆盖伴随对象中的invoke操作符

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)在元素类型上覆盖invoke操作符

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

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

2 个答案:

答案 0 :(得分:1)

我认为不可避免地要为您创建的每个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可以使用自己的{{ 1}}功能。

答案 1 :(得分:0)

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

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

所以我推翻了一元+运算符

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