Обязательные и один из нескольких идиом - PullRequest
0 голосов
/ 06 января 2019

Поддержка Kotlin DSL отличная, но я столкнулся с двумя сценариями, я могу только добавить обходной путь. Оба обходных пути имеют свой главный недостаток, поскольку они навязывают ограничения только во время выполнения.

Первое ограничение: обязательный параметр

Я хотел бы написать что-то вроде этого:

start {
    position {
        random {
            rect(49, 46, 49, 47)
            rect(50, 47, 51, 48)
            point(51, 49)
        }
    }
}

где положение является обязательным параметром. Мой подход состоит в том, чтобы установить нулевое положение при запуске и проверить его при создании начального объекта.

Второе ограничение: одно из многих

Я хотел бы разрешить ровно один из нескольких возможных подобъектов:

start {
    position {
        random {
            [parameters of random assign]
        }
    }
}

or

start {
    position {
        user {
            [parameters of user assign]
        }
    }
}

У меня ощущение, что я достиг предела возможностей инструментария Kotlin DSL, потому что эти требования также проверяются только на время компиляции на базовом языке.

Есть идеи?

Ответы [ 2 ]

0 голосов
/ 07 января 2019

Подумав о проблеме, я понял, что эти два требования не могут быть решены в самом Kotlin, поэтому невозможно получить чисто синтаксическое решение в текущей форме, представленной выше. Однако есть несколько опций, которые могут создать достаточно близкий синтаксис и решить одну или обе проблемы одновременно.

Вариант 1: Параметры

Это решение довольно простое и уродливое, добавляя ужасную аномалию "где находится закрывающая скобка". Он просто перемещает свойство position в конструктор:

start(random {
    rect(49, 46, 49, 47)
    rect(50, 47, 51, 48)
    point(51, 49)
}) {
    windDirection to NORTH
    boat turn (BEAM_REACH at STARBOARD)
} 

Это просто в коде:

    fun start(pos : StartPosition, op: StartConfigBuilder.() -> Unit) : StartConfigBuilder 
             = StartConfigBuilder(pos).apply(op)

и создает функции построителя верхнего уровня для реализации позиции:

fun random( op : RandomStartPositionBuilder.() -> Unit) = RandomStartPositionBuilder().apply(op).build()

class RandomStartPositionBuilder {
    private val startZoneAreas = mutableListOf<Area>()

    fun rect(startRow: Int, startColumn: Int, endRow: Int = startRow, endColumn: Int) =
            startZoneAreas.add(Area(startRow, startColumn, endRow, endColumn))

    fun point(row: Int, column: Int) = startZoneAreas.add(Area(row, column))

    fun build() = RandomStartPosition(if (startZoneAreas.isEmpty()) null else Zone(startZoneAreas))
}

fun user( op : UserStartPositionBuilder.() -> Unit) = UserStartPositionBuilder().apply(op).build()

class UserStartPositionBuilder {

    fun build() = UserStartPosition()
}

Хотя это решает как необходимые, так и только одни проблемы во время редактирования, значительно затрудняет чтение DSL и мы теряем элегантность инструментов DSL. Это станет еще более беспорядочным, если в конструктор будет перенесено более одного свойства или внутренний объект (позиция) станет более сложным.

Вариант 2: инфиксная функция

Это решение перемещает необходимое сложное поле за пределы блока (это «неприятная» часть) и использует его как инфиксную функцию:

start {
    windDirection to NORTH
    boat turn (BEAM_REACH at STARBOARD)
} position random {
    rect(49, 46, 49, 47)
    rect(50, 47, 51, 48)
    point(51, 49)
}

or 

start {
    windDirection to NORTH
    boat turn (BEAM_REACH at STARBOARD)
} position user {
}

Это решение решает «единственную» проблему, но не «точно одну».

Чтобы добиться этого, я изменил строители:

//Note, that the return value is the builder: at the end, we should call build() later progmatically
fun start(op: StartConfigBuilder.() -> Unit) : StartConfigBuilder = StartConfigBuilder().apply(op)


class StartConfigBuilder {
    private var position: StartPosition = DEFAULT_START_POSITION
    private var windDirectionVal: InitialWindDirection = RandomInitialWindDirection()

    val windDirection = InitialWindDirectionBuilder()
    val boat = InitialHeadingBuilder()

    infix fun position(pos : StartPosition) : StartConfigBuilder {
        position = pos
        return this
    }

    fun build() = StartConfig(position, windDirection.value, boat.get())
}

// I have to move the factory function top level
fun random( op : RandomStartPositionBuilder.() -> Unit) = RandomStartPositionBuilder().apply(op).build()

class RandomStartPositionBuilder {
    private val startZoneAreas = mutableListOf<Area>()

    fun rect(startRow: Int, startColumn: Int, endRow: Int = startRow, endColumn: Int) =
            startZoneAreas.add(Area(startRow, startColumn, endRow, endColumn))

    fun point(row: Int, column: Int) = startZoneAreas.add(Area(row, column))

    fun build() = RandomStartPosition(if (startZoneAreas.isEmpty()) null else Zone(startZoneAreas))
}

// Another implementation 
fun user( op : UserStartPositionBuilder.() -> Unit) = UserStartPositionBuilder().apply(op).build()

class UserStartPositionBuilder {

    fun build() = UserStartPosition()
}

Это решает проблему реализации "only-one" почти элегантным способом, но не дает ответа на параметр "required property". Поэтому хорошо, когда может быть применено значение по умолчанию, но все же выдает только исключение времени разбора, когда позиция отсутствует.

Опции 3: цепочка инфиксных функций

Это решение является вариантом предыдущего. Для решения требуемой проблемы предыдущего мы используем переменную и промежуточный класс:

var start : StartWithPos? = null

class StartWithoutPos {
    val windDirection = InitialWindDirectionBuilder()
    val boat = InitialHeadingBuilder()
}

class StartWithPos(val startWithoutPos: StartWithoutPos, pos: StartPosition) {
}

fun start( op: StartWithoutPos.() -> Unit): StartWithoutPos {
    val res = StartWithoutPos().apply(op)
    return res
}

infix fun StartWithoutPos.position( pos: StartPosition): StartWithPos {
    return StartWithPos(this, pos)
}

Тогда мы могли бы написать следующее утверждение в DSL:

start = start {
    windDirection to NORTH
    boat heading NORTH
} position random {
}

Это решит обе проблемы, но со стоимостью дополнительного назначения переменной.

Все три решения работают, добавляют немного грязи в DSL, но можно было бы выбрать, которое подходит лучше.

0 голосов
/ 07 января 2019

Вы можете черпать вдохновение из собственного HTML DSL Kotlin. Для обязательных аргументов используйте простые функции с аргументами, а не литерал функции с получателем.

Ваш DSL будет выглядеть примерно так:

start(
    position {// This is mandatory
        random {// This is not

        }
    }
)

И ваш start строитель:

fun start(position: Position): Start {
    val start = Start(position)
    ...
    return start
}

Используйте тот же подход для position().

...