在Kotlin中,如何针对不同的设置分支限制流畅的Builder中的选择

Jay*_*ard 6 kotlin

在Kotlin,我正在编写一个构建器,并且需要一系列明显且必须完成的步骤.使用流畅的构建器,我可以显示所有步骤,但不能确定它们必须发生的顺序,也不能根据上一步更改哪些步骤可用.所以:

serverBuilder().withHost("localhost")
         .withPort(8080)
         .withContext("/something")
         .build()
Run Code Online (Sandbox Code Playgroud)

很好,但随后添加SSL证书等选项:

serverBuilder().withHost("localhost")
         .withSsl()
         .withKeystore("mystore.kstore")
         .withContext("/secured")
         .build()
Run Code Online (Sandbox Code Playgroud)

现在没有什么能阻止非ssl版本拥有withKeystore和其他选项.在没有首先打开的情况下调用此SSL方法时应该出错withSsl():

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

在路上有更多的叉子可能会更复杂,我只想要一些物品而不是其他物品.

如何限制构建器逻辑中每个分支可用的功能?这对建筑商来说是不可能的,而应该是DSL吗?

注意: 这个问题是由作者故意编写和回答的(答案问题),因此对于常见问题的Kotlin主题的惯用答案存在于SO中.

Jay*_*ard 3

您需要将您的构建器更多地视为具有一系列类的 DSL,而不仅仅是一个类;即使坚持构建者模式。语法的上下文会更改当前处于活动状态的构建器类。

让我们从一个简单的选项开始,仅当用户在 HTTP(默认)和 HTTPS 之间进行选择时,才分叉构建器类,从而保持构建器的感觉:

我们将使用一个快速扩展函数来使流畅的方法更漂亮:

fun <T: Any> T.fluently(func: ()->Unit): T {
    return this.apply { func() }
}
Run Code Online (Sandbox Code Playgroud)

现在主要代码:

// 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"))
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

以及一个帮助函数来启动构建器以匹配上面问题中的代码:

fun serverBuilder(): HttpServerBuilder {
    return HttpServerBuilder()
}
Run Code Online (Sandbox Code Playgroud)

在此模型中,我们使用一个内部类,该内部类可以继续对构建器的某些值进行操作,并可选择携带其自己的唯一值和最终的唯一验证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
}
Run Code Online (Sandbox Code Playgroud)

嵌套类通过此接口委托给外部类:

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"))
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我们最终得到了相同的净效应。如果您有其他分叉,您可以继续打开继承界面,并在每个级别的新后代中添加每个级别的设置。

尽管第一个示例可能由于设置数量较少而较小,但当设置数量更多时,情况可能会相反,并且我们在道路上有更多的岔路口,正在构建越来越多的设置,然后是界面+委托模型可能不会节省大量代码,但它会减少您忘记要委托的特定方法或具有与预期不同的方法签名的机会。

这是两个模型之间的主观差异。

关于改用 DSL 样式生成器:

如果您使用 DSL 模型,例如:

Server {
    host = "localhost" 
    port = 80  
    context = "/secured"
    ssl {
        keystore = "mystore.kstore"
        password = "p@ssw0rd!"
    }
}
Run Code Online (Sandbox Code Playgroud)

您的优势在于,您不必担心委托设置或方法调用的顺序,因为在 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"
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

因此,您无法阻止某些 HTTP 属性渗入 HTTPS 范围。这打算在KT-11551中修复,请参阅此处了解更多详细信息:Kotlin - 限制扩展方法范围