Котлин - расширяемые типобезопасные строители - PullRequest
0 голосов
/ 09 сентября 2018

Я хочу иметь возможность создавать пользовательский объект типа 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"))
    })
}

Но это, очевидно, не чисто и просто откладывает громоздкость на сторону использования вместо стороны реализации.

Ответы [ 2 ]

0 голосов
/ 10 сентября 2018

Я придумала решение, которое не самое элегантное, но оно сносно и работает так, как мне бы хотелось.

Оказывается, что если вы переопределите оператор (или создадите любую функцию расширения) внутри класса, он получит доступ к своему родительскому контексту.

Итак, я перебил унарный оператор +

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()
        }
0 голосов
/ 09 сентября 2018

Я думаю, что неизбежно добавлять какую-то вспомогательную функцию для каждого создаваемого вами подкласса 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 может быть создан с его собственной функцией newElem .

...