在Kotlin中,如何限制流利的Builder中的各种设置的选择

在Kotlin,我正在写一个建筑师,想要一系列明显的步骤,必须完成。 有了一个流利的建筑师,我可以提出所有的步骤,但并不是真正地设定它们必须发生的顺序,也不能根据前面的步骤更改哪些是可用的。 所以:

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() 

在道路上我只想要一些物体,而不是其他物体,这可能会更复杂。

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

注意: 这个问题是由作者故意写的和回答的( 自我回答的问题 ),所以对于常见的Kotlin话题的习惯性的回答是在SO中。

你需要把你的建设者看作是一系列类的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()调用将用户的上下文转换为此内部类。

因此,用户仅限于在每个“叉路”中允许的选项。 在withKeystore()之前调用withKeystore()不再被允许。 你有你想要的错误。

这里的一个问题是,您必须从内部类手动委派任何您想要继续工作的设置。 如果这是一个很大的数字,这可能是烦人的。 相反,您可以将通用设置变为一个接口,并使用类委托从嵌套类委派给外部类。

所以这里是重构使用通用接口的生成器:

 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风格构建器来代替:

如果您使用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 – 限制扩展方法范围