【问题标题】:Kotlin - extensible type-safe buildersKotlin - 可扩展的类型安全构建器
【发布时间】: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"))
    })
}

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

【问题讨论】:

    标签: kotlin factory builder


    【解决方案1】:

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


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

    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 .

    【讨论】:

      【解决方案2】:

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

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

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-04-02
        • 2023-03-04
        • 1970-01-01
        • 2018-10-06
        • 1970-01-01
        相关资源
        最近更新 更多