Я хочу иметь возможность создавать пользовательский объект типа 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")
}
}
Это могло бы сработать, за исключением случая, когда вы сразу пытаетесь вызвать объект, созданный вызовом конструктора, компилятор не может сказать, что лямбда-выражение для вызова "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"))
})
}
Но это, очевидно, не чисто и просто откладывает громоздкость на сторону использования вместо стороны реализации.