本博客中,我们将介绍有关Kotlin Builder模式的几个方面。我们了解如何创建Builder模式以及是否应该在Kotlin中使用它
一、对象的配置
许多程序员使用一种模式来连接成员函数的调用,以避免重新键入对象的名称。此模式通常与Builder模式本身结合使用。人们必须小心,因为有些人混淆了术语并说连接是Builder模式。实际上,一些开发人员会抱怨这不是“Kotlin方式”(例如在StackOverflow的一些论坛中)。然而,我们认为这种配置对象的方法是许多程序员都知道的并且非常明确。所以不适用它是没有意义的。
以下代码就是一个示例。
// 没有链接
val person = Person()
person.setName("Tom")
person.setAddress("Street")
person.setAge(18)
// 带链接
val person = Person()
person
.setName("Tom")
.setAddress("Street")
.setAge(18)
第一种方法的问题是每行都必须重写对象名称。由于典型的复制粘贴错误,这会导致潜在的错误。以下代码可以正确编译,但不是您想要的。在进行复制和粘贴时,经常会发生这种错误。
//赋值粘贴错误
val person = Person()
person.name = "Tom"
person.address = "Street"
person.age = 18
val person2 = Person()
person.name = "Tom"
person.address = "Street"
person.age = 18
因此,第二种方法要好得多。您很少需要修改对象的名称。这会减少错误。要实现这种行为,您需要在函数调用中返回对象本身。Kotlin原生提供了一个扩展函数apply来为您完成这项工作。
1.1、作用域函数 - apply
扩展函数apply采用lambda函数并提供所配置对象的所有公共变量的访问。使用apply的优点是它是内置的,因此不需要任何样板代码。特别是对于数据类,这是最好的方法。在下一个代码示例中,我们首先展示如何以传统方式使用链接。这是大多数程序员所熟悉的版本。第二个版本使用kotln的内置功能。由于第二版本需要较少的代码,因此它是首选方式。
// 一般方式
class Person() {
var name = ""
var address = ""
var age = 0
fun setName(name: String): Person {
this.name = name
return this
}
fun setAddress(address: String): Person {
this.address = address
return this
}
fun setAge(age: Int): Person {
this.age = age
return this
}
}
// 使用apply
class Person() {
var name = ""
var address = ""
var age = 0
fun setName(name: String) = apply {
this.name = name
}
fun setAddress(address: String) = apply {
this.address = address
}
fun setAge(age: Int) = apply {
this.age = age
}
}
请注意,apply也可以在客户端(使用该对象的函数)用作扩展函数。这对于数据类特别有用。但是,通过这种方式,您无法访问私有成员/函数(正如预期的那样)。
fun main() {
val person = Person()
person
.setName("Tom")
.setAddress("Street")
.setAge(18)
val person2 = Person()
person2.apply {
name = "Tom"
address = "Street"
age = 18
// only access to public properties
}
}
二、Lombok库
Kotlin 提供了一个实验性的Lombok编译器插件。要使用它,您必须按照KotlinLang中的讨论安装插件。由于它仍处于实验阶段,我们不建议使用它。Kotlin Lombok编译器插件允许Kotlin 代码在同一个混合 Java/Kotlin 模块中生成和使用 Java 的Lombok声明。 如果准备好,它将允许诸如 @builder 之类的注释,这将自动创建所有必需的代码。
三、应用业务规则
使用这种设计模式的另一个方面是,它为提供专用软件层提供了完美的场所。该层完全负责应用有关域对象创建的业务规则。在以下示例中,可以使用PersonBuilder
构建域对象Person
。PersonBuilder
包含所有相关逻辑来检查是否可以构建Person。这又不是 GoF 书中描述的实际“构建器模式”。
class Person() {
var name = ""
var address = ""
var age = 0
}
class PersonBuilder() {
private var name = ""
private var address = ""
private var age = 0
fun setName(name: String) = apply {
this.name = name
}
fun setAddress(address: String) = apply {
this.address = address
}
fun setAge(age: Int) = apply {
this.age = age
}
fun canBuild(): Boolean {
// do business rule, checks
return true
}
fun build(): Person {
val person = Person()
if (canBuild()) {
person.address = address
person.name = name
person.age = age
}
return person
}
}
四、单独的配置和实例化
使用此模式的另一个原因是将对象的构造和配置分开。这不仅是因为将这两个问题分开可能更容易。但也可能当时并非所有信息都可用。人们可以想象,在用户界面中我们可以配置一个弹出窗口。点击启动按钮时,会创建弹窗。创建和配置在时间上是完全分开的。
五、DSL语言
Build设计模式的另一个方面是创建DSL语言。DSL代表特定领域语言。Kotlin能够使用命名良好的函数作为构建器,结合函数文字作为接收器,创建类型安全、静态类型的构建器。这允许创建类型安全的特定于域的语言(DSL),适合以半声明的方式构建复杂的分层数据结构。例如想象一下下面的代码
// 组合对象
class Address {
var city = ""
var street = ""
}
class Person {
var name = ""
var age = 0
var address = Address()
}
// 陈述性协作
val person = person {
name = "John"
age = 18
address {
city = "New York"
street = "Main Street"
}
}
要实现这一点,您必须使用函数、函数名称和lambda函数。在这种情况下使用以下内容:
// DSL 示例
class Address {
var city = ""
var street = ""
}
class Person {
var name = ""
var age = 0
var address = Address()
fun address(init: Address.() -> Unit) {
val address = Address()
address.init()
this.address = address
}
}
fun person(init: Person.() -> Unit): Person {
val person = Person()
person.init()
return person
}
这段代码有什么作用?首先我们声明了一个名为person
的函数。此函数接受带有人员上下文的lambda函数(init)。这允许访问lambda函数中的公共属性。其次,我们在Person类中创建一个新函数address
。该功能与Person的功能类似。
六、新功能:Kotlin TypeSafe DSL Builder
接下来会有单独一篇博客,介绍Kotlin领域特定语言支持的优缺点和最佳实践,敬请期待~
七、Kotlin中的Builder模式
从现在开始,我们将展示如何在Kotlin中实现实际的Builder模式。我们参考《设计模式》一书中描述的额实际方式。然而,我们不会像书中那样详细介绍。我们将更多地关注Kotlin的实现方面。
将复杂对象的的构造与其表示分离,以便相同的构造过程可以创建不同的表示
构造器 - 设计模式/GoF
总体结构如下:
创建一个基本构架器类(AbstractBuilder
),它定义某些函数(actionA()
等)。在基本实现中,这些函数通常是空的。它们将在构建器类(ContreteBuilderA
等)的具体实现中被覆盖。这些具体实现还定义了返回特定蟾片的构建函数。请注意,产品中可能会有很大不同。因此它们不一定需要具有相同的接口。
八、替代方案:抽象工厂
Build模式的一个常见替代方案是使用抽象工厂。这种设计模式还涉及对象构建过程的分离。区别在于抽象工厂返回抽象对象,而构建器模式通常返回具体对象。在抽象工厂中,客户端不需要知道实例化了哪种对象,而在构建器模式中,客户端则知道。
九、私有构造
为了提供更多的安全性,可以将具体对象的构造函数声明为私有。唯一的公共构造函数将接受在init块中构建对象的构建器。
十、例子
考虑以下示例。在用户界面中,可以定义弹出窗口的信息。这包括Title
、Text
、ActionButton
和CancelButton
。信息存储为json
、xml
和Widget
。三种不同形式的创建是由建造者完成的。
// 弹出窗口信息的数据类
data class PopupWindowInfo(
val title: String?,
val text: String?,
val actionButton: String?,
val cancelButton: String?
)
// 弹出窗口格式的接口
interface PopupFormat {
fun format(info: PopupWindowInfo): String
}
// JSON格式的弹出窗口
class JsonPopupFormat: PopupFormat {
override fun format(info: PopupWIndowInfo): String {
return """
{
"title": "${info.title}",
"text": "${info.text}",
"actionButton": "${info.actionButton}",
"cancelButton": "${info.cancelButton}"
}
""".trimIndent()
}
}
// XML格式的弹出窗口
class XmlPopupFormat: PopupFormat {
override fun format(info: PopupWindowInfo): String {
return """
<popup>
<title>${info.title}</title>
<text>${info.text}</text>
<actionButton>${info.actionButton}</actionButton>
<cancelButton>${info.cancelButton}</cancelButton>
</popup>
""".trimIndent()
}
}
// Widget格式的弹出窗口
class WidgetPopupFormat: PopupFormat {
override fun format(info: PopupWindowInfo): String {
//在这里实现将信息渲染为Widget的逻辑
return "Widget representation of popup info"
}
}
// 构造器类
class PopupWindowBuilder {
private var title: String? = null
private var text: String? = null
private var actionButton: String? = null
private var cancelButton: String? = null
fun setTitle(title: String) = apply {
this.title = title
}
fun setText(text: String) = apply {
this.text = text
}
fun setActionButton(actionButton: String) = apply {
this.actionButton = actionButton
}
fun setCancelButton(cancelButton: String) = apply {
this.cancelButton = cancelButton
}
fun build(format: PopupFormat): String {
var popupInfo = PopupWindowInfo(title, text, actionButton, cancelButton)
return format.format(popupInfo)
}
}
fun main() {
// 创建一个弹出窗口的信息的Builder
val builder = PopupWindowBuilder()
.setText("Sample Title")
.setText("This is the content of the popup window.")
.setActionButton("OK")
.setCancelButton("Cancel")
//创建不同格式的弹出窗口
val jsonFormat = JsonPopupFormat()
val xmlFormat = XmlPopupFormat()
val widgetFormat = WidgetPopupFormat()
val jsonPopup = builder.build(jsonFormat)
val xmlPopup = builder.build(xmlFormat)
val widgetPopup = builder.build(widgetFormat)
println("JSON Popup")
println(jsonPopup)
println("\nXML Popup")
println(xmlPopup)
println("\nWidget Popup")
println(widgetPopup)
}
在这个完整的例子中,我们创建了不同格式的弹出窗口,包括JSON、XML和Widget。每个格式都有一个对应的类(JsonPopupFormat
、XmlPopupFormat
和WidgetPopupFormat
)来实现格式化逻辑。PopupWindowBuilder
的build()
方法现在接受一个格式参数,并使用适当的格式器来生成相应的信息。
在main
函数中,我们首先创建一个弹出窗口信息的Builder
,然后使用不同的格式来构建弹出窗口,并输出它们的格式化结果。这个例子演示了如何根据不同的格式要求使用Builder
模式来创建和输出弹出窗口信息。