Kotlin设计模式之Builder

本博客中,我们将介绍有关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构建域对象PersonPersonBuilder包含所有相关逻辑来检查是否可以构建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

image.png

总体结构如下:

创建一个基本构架器类(AbstractBuilder),它定义某些函数(actionA()等)。在基本实现中,这些函数通常是空的。它们将在构建器类(ContreteBuilderA等)的具体实现中被覆盖。这些具体实现还定义了返回特定蟾片的构建函数。请注意,产品中可能会有很大不同。因此它们不一定需要具有相同的接口。

八、替代方案:抽象工厂

Build模式的一个常见替代方案是使用抽象工厂。这种设计模式还涉及对象构建过程的分离。区别在于抽象工厂返回抽象对象,而构建器模式通常返回具体对象。在抽象工厂中,客户端不需要知道实例化了哪种对象,而在构建器模式中,客户端则知道。

九、私有构造

为了提供更多的安全性,可以将具体对象的构造函数声明为私有。唯一的公共构造函数将接受在init块中构建对象的构建器。

十、例子

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

// 弹出窗口信息的数据类
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模式来创建和输出弹出窗口信息。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

全套视频资料:

一、面试合集

在这里插入图片描述
二、源码解析合集
在这里插入图片描述

三、开源框架合集
在这里插入图片描述
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓

猜你喜欢

转载自blog.csdn.net/Code1994/article/details/132547595