【发布时间】:2018-09-10 05:44:07
【问题描述】:
我希望能够创建一个自定义的构建器模式 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) 覆盖伴生对象中的调用操作符
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 是用于“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"))
})
}
但这显然不干净,只是将繁琐性推迟到使用方面而不是实现方面。
【问题讨论】: