我希望能够创建自定义的构建器模式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
来创建一个新元素。
如果可能的话,我不想使用反射。
我的主要问题是提出一个干净的解决方案。我想到了其他一些没有成功的方法。
// Pre-defined
fun createElement(...): (Element.() -> Unit) -> Element
// Created as
val newelem = createElement(...)
// Used as
body {
newelem {
p { +"newelem example" }
}
}
这样做有明显的弊端,我也没有实现它的明确方法-可能涉及反思。
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" }
}
}
不幸的是,无法强制类型子类型以子类型实现的“静态”功能。
此外,伴随对象也不会被继承,因此对子类的调用无论如何都不会起作用。
我们再次遇到了将子元素添加到正确上下文的问题,因此构建器实际上并没有构建任何东西。
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")
}
}
但是再说一次,如果没有反射或类似的操作,实际上不可能将子元素添加到父元素中,因此构建器实际上并没有构建任何东西。
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"))
})
}
但这显然不是干净的,只是将繁琐的工作推迟到了使用方面,而不是在实现方面。
答案 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()
}