В Kotlin, как вы можете ограничить выбор в fluent Builder для разных вилок настроек

в Котлине я пишу строителя и хочу, чтобы ряд шагов, которые очевидны и должны быть завершены. С помощью fluent builder я могу представить все шаги, но не установить порядок их выполнения, а также изменить, какие из них доступны на основе предыдущего шага. Итак:

serverBuilder().withHost("localhost")
         .withPort(8080)
         .withContext("/something")
         .build()

отлично, но затем добавление опций, таких как SSL-сертификаты:

serverBuilder().withHost("localhost")
         .withSsl()
         .withKeystore("mystore.kstore")
         .withContext("/secured")
         .build()

теперь ничто не мешает версии без ssl иметь withKeystore и другие варианты. Должна быть ошибка при вызове этого метода SSL без предварительного включения withSsl():

serverBuilder().withHost("localhost")
         .withPort(8080)
         .withContext("/something")
         .withKeystore("mystore.kstore")   <------ SHOULD BE ERROR!
         .build()

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

как ограничить, какие функции доступны на каждой вилке в логике builder? Это невозможно для строителя, а вместо этого должно быть DSL?

Примечание: этот вопрос намеренно написан и на него ответил автор (Вопросы С Ответами), так что идиоматические ответы на часто задаваемые темы Котлина присутствуют в SO.

1 ответов


вам нужно думать о вашем Строителе как о более DSL с серией классов вместо одного класса; даже если придерживаться шаблона строителя. Контекст грамматики изменяется, какой класс builder в данный момент активен.

давайте начнем с простой опции, которая развивает класс builder только тогда, когда пользователь выбирает между HTTP (по умолчанию) и HTTPS, сохраняя ощущение builder:

быстрая функция расширения которую мы будем использовать для того чтобы сделать беглые методы красивее:

fun <T: Any> T.fluently(func: ()->Unit): T {
    return this.apply { func() }
}

теперь основной код:

// our main builder class
class HttpServerBuilder internal constructor () {
    private var host: String = "localhost"
    private var port: Int? = null
    private var context: String = "/"

    fun withHost(host: String) = fluently { this.host = host }
    fun withPort(port: Int) = fluently { this.port = port }
    fun withContext(context: String) = fluently { this.context = context }

    // !!! transition to another internal builder class !!!
    fun withSsl(): HttpsServerBuilder = HttpsServerBuilder()

    fun build(): Server = Server(host, port ?: 80, context, false, null, null)

    // our context shift builder class when configuring HTTPS server
    inner class HttpsServerBuilder internal constructor () {
        private var keyStore: String? = null
        private var keyStorePassword: String? = null

        fun withKeystore(keystore: String) = fluently { this.keyStore = keyStore }
        fun withKeystorePassword(password: String) = fluently { this.keyStorePassword = password }

        // manually delegate to the outer class for withPort and withContext
        fun withPort(port: Int) = fluently { this@HttpServerBuilder.port = port }
        fun withContext(context: String) = fluently { this@HttpServerBuilder.context = context }

        // different validation for HTTPS server than HTTP
        fun build(): Server {
            return Server(host, port ?: 443, context, true,
                    keyStore ?: throw IllegalArgumentException("keyStore must be present for SSL"),
                    keyStorePassword ?: throw IllegalArgumentException("KeyStore password is required for SSL"))
        }
    }
}

и вспомогательная функция для начала строителя, чтобы соответствовать вашему коду в вопросе выше:

fun serverBuilder(): HttpServerBuilder {
    return HttpServerBuilder()
}

в этой модели мы используем внутренний класс, который может продолжать работать с некоторыми значениями построителя и необязательно нести свои собственные уникальные значения и уникальную проверку окончательного build(). Конструктор преобразует контекст пользователя в этот внутренний класс на withSsl() звонок.

поэтому пользователь ограничен только параметрами, разрешенными на каждой "развилке дороги". Зову withKeystore() до withSsl() больше не пускают. У вас есть ошибка, которую вы желаете.

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

так вот строитель рефакторинг, чтобы использовать общий интерфейс:

private interface HttpServerBuilderCommon {
    var host: String
    var port: Int?
    var context: String

    fun withHost(host: String): HttpServerBuilderCommon
    fun withPort(port: Int): HttpServerBuilderCommon
    fun withContext(context: String): HttpServerBuilderCommon

    fun build(): Server
}

с вложенным классом, делегирующим через этот интерфейс внешнему:

class HttpServerBuilder internal constructor (): HttpServerBuilderCommon {
    override var host: String = "localhost"
    override var port: Int? = null
    override var context: String = "/"

    override fun withHost(host: String) = fluently { this.host = host }
    override fun withPort(port: Int) = fluently { this.port = port }
    override fun withContext(context: String) = fluently { this.context = context }

    // transition context to HTTPS builder
    fun withSsl(): HttpsServerBuilder = HttpsServerBuilder(this)

    override fun build(): Server = Server(host, port ?: 80, context, false, null, null)

    // nested instead of inner class that delegates to outer any common settings
    class HttpsServerBuilder internal constructor (delegate: HttpServerBuilder): HttpServerBuilderCommon by delegate {
        private var keyStore: String? = null
        private var keyStorePassword: String? = null

        fun withKeystore(keystore: String) = fluently { this.keyStore = keyStore }
        fun withKeystorePassword(password: String) = fluently { this.keyStorePassword = password }

        override fun build(): Server {
            return Server(host, port ?: 443, context, true,
                    keyStore ?: throw IllegalArgumentException("keyStore must be present for SSL"),
                    keyStorePassword ?: throw IllegalArgumentException("KeyStore password is required for SSL"))
        }
    }
}

мы в конечном итоге с тем же чистый эффект. Если у вас есть дополнительные вилки, вы можете продолжать открывать интерфейс для наследования и добавлять настройки для каждого уровня в новый потомок для каждого уровня.

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

это субъективная разница между двумя модели.

об использовании DSL style builder вместо этого:

если вы использовали модель DSL вместо этого, например:

Server {
    host = "localhost" 
    port = 80  
    context = "/secured"
    ssl {
        keystore = "mystore.kstore"
        password = "p@ssw0rd!"
    }
}

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

Server {
    host = "localhost" 
    port = 80  
    context = "/secured"
    ssl {
        keystore = "mystore.kstore"
        password = "p@ssw0rd!"
        ssl {
            keystore = "mystore.kstore"
            password = "p@ssw0rd!"
            ssl {
                keystore = "mystore.kstore"
                password = "p@ssw0rd!"
                port = 443
                host = "0.0.0.0"
            }
        }
    }
}

таким образом, вы не можете предотвратить кровотечение некоторых свойств HTTP в область HTTPS. Это должно быть исправлено в KT-11551, Подробнее см. здесь: Kotlin-ограничить область метода расширения