Kotlin设计模式之Builder

本博客中,我们将介绍有关Kotlin Builder模式的几个方面。我们了解如何创建Builder模式以及是否应该在Kotlin中使用它

一、对象的配置

许多程序员使用一种模式来连接成员函数的调用,以避免重新键入对象的名称。此模式通常与Builder模式本身结合使用。人们必须小心,因为有些人混淆了术语并说连接是Builder模式。实际上,一些开发人员会抱怨这不是"Kotlin方式"(例如在StackOverflow的一些论坛中)。然而,我们认为这种配置对象的方法是许多程序员都知道的并且非常明确。所以不适用它是没有意义的。

以下代码就是一个示例。

scss 复制代码
// 没有链接
val person = Person()
person.setName("Tom")
person.setAddress("Street")
person.setAge(18)
scss 复制代码
// 带链接
val person = Person()
person
    .setName("Tom")
    .setAddress("Street")
    .setAge(18)

第一种方法的问题是每行都必须重写对象名称。由于典型的复制粘贴错误,这会导致潜在的错误。以下代码可以正确编译,但不是您想要的。在进行复制和粘贴时,经常会发生这种错误。

ini 复制代码
//赋值粘贴错误
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的内置功能。由于第二版本需要较少的代码,因此它是首选方式。

kotlin 复制代码
// 一般方式
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
    }
}
kotlin 复制代码
// 使用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也可以在客户端(使用该对象的函数)用作扩展函数。这对于数据类特别有用。但是,通过这种方式,您无法访问私有成员/函数(正如预期的那样)。

kotlin 复制代码
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构建域对象PersonPersonBuilder包含所有相关逻辑来检查是否可以构建Person。这又不是 GoF 书中描述的实际"构建器模式"。

kotlin 复制代码
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),适合以半声明的方式构建复杂的分层数据结构。例如想象一下下面的代码

ini 复制代码
// 组合对象
class Address {
    var city = ""
    var street = ""
}

class Person {
    var name = ""
    var age = 0
    var address = Address()
}
ini 复制代码
// 陈述性协作
val person = person {
    name = "John"
    age = 18
    
    address {
        city = "New York"
        street = "Main Street"
    }
}

要实现这一点,您必须使用函数、函数名称和lambda函数。在这种情况下使用以下内容:

kotlin 复制代码
// 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块中构建对象的构建器。

十、例子

考虑以下示例。在用户界面中,可以定义弹出窗口的信息。这包括TitleTextActionButtonCancelButton。信息存储为jsonxmlWidget。三种不同形式的创建是由建造者完成的。

kotlin 复制代码
// 弹出窗口信息的数据类
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。每个格式都有一个对应的类(JsonPopupFormatXmlPopupFormatWidgetPopupFormat)来实现格式化逻辑。PopupWindowBuilderbuild()方法现在接受一个格式参数,并使用适当的格式器来生成相应的信息。

main函数中,我们首先创建一个弹出窗口信息的Builder,然后使用不同的格式来构建弹出窗口,并输出它们的格式化结果。这个例子演示了如何根据不同的格式要求使用Builder模式来创建和输出弹出窗口信息。

相关推荐
黄林晴2 小时前
如何判断手机是否是纯血鸿蒙系统
android
火柴就是我2 小时前
flutter 之真手势冲突处理
android·flutter
法的空间3 小时前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
循环不息优化不止3 小时前
深入解析安卓 Handle 机制
android
恋猫de小郭3 小时前
Android 将强制应用使用主题图标,你怎么看?
android·前端·flutter
jctech3 小时前
这才是2025年的插件化!ComboLite 2.0:为Compose开发者带来极致“爽”感
android·开源
用户2018792831673 小时前
为何Handler的postDelayed不适合精准定时任务?
android
叽哥3 小时前
Kotlin学习第 8 课:Kotlin 进阶特性:简化代码与提升效率
android·java·kotlin
Cui晨3 小时前
Android RecyclerView展示List<View> Adapter的数据源使用View
android
氦客3 小时前
Android Doze低电耗休眠模式 与 WorkManager
android·suspend·休眠模式·workmanager·doze·低功耗模式·state_doze