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

时间:2016-09-17 01:41:11

标签: kotlin

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

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

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

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

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

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

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

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

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

1 个答案:

答案 0 :(得分:3)

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

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

我们将使用快速扩展功能使流畅的方法更漂亮:

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()调用时将用户的上下文转换为此内部类。

因此,用户仅限于道路上每个&#34;叉所允许的选项&#34>。不再允许在withKeystore()之前致电withSsl()。你有你想要的错误。

这里的一个问题是,您必须手动将内部类委托给您要继续工作的任何设置。如果这是一个很大的数字,这可能会很烦人。相反,您可以在界面中进行常规设置,并使用class delegation从嵌套类委托给外部类。

所以这里是重构使用公共接口的构建器:

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"))
        }
    }
}

我们最终会产生相同的净效应。如果您有其他分支,则可以继续打开继承接口,并为每个级别的新后代添加每个级别的设置。

虽然第一个例子可能由于设置数量较少而较小,但是当设置数量更多并且我们在道路上有更多的叉子构建越来越多的设置时,它可能是相反的,那么interface + delegation模型可能无法保存大量代码,但它会减少您忘记特定方法委派或具有与预期不同的方法签名的机会。

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

关于使用DSL样式构建器:

如果您使用的是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 - Restrict extension method scope