Kotlin如何实现构建者模式

[版权申明] 非商业目的注明出处可自由转载
博文地址: http://shusheng007.top/2020/10/14/1-7/
出自:shusheng007

系列文章:
秒懂设计模式之建造者模式(Builder pattern)

@[toc]

前言

构建者模式在Java世界中是一个非常实用的创建型设计模式,日常使用非常频繁。有不熟悉的小伙伴请移步到 秒懂设计模式之建造者模式(Builder pattern)进行回顾。但是当Java的挑战者Kotlin横空出世后,很多人说此设计模式失去了其用武之地,事实真的是这样吗?

你内心中是否在期待翻转剧情呢?现实往往是要打脸的,在大部分情况下,事实真的是这样!

默认参数构建对象

由于Kotlin支持默认参数名称参数,使得Kotlin在大部分情况下不需要构建者模式就能很好的完成对象创建任务,talk is cheep, show me the code,我想必须用code来征服你们。

需求:

我们要根据不同的配置来构建一个电脑类的实例,希望可以自由配置电脑CPU、RAM、显示器、键盘以及USB端口,从而组装出不同的Computer实例。

如果使用Java来实现的话,最好的选择就是使用构建者模式,正如 秒懂设计模式之建造者模式(Builder pattern)中所展示的那样。但是如果使用Kotlin实现的话,默认参数与名称参数就可以很好的完成需求,如下代码所示:

class Computer(
    val cpu: String = "英特尔",
    val ram: String = "金士顿",
    val usbCount: Int = 2,
    val keyboard: String = "罗技",
    val display: String = "京东方"
) {
    override fun toString(): String {
        return "Computer(cpu='$cpu', ram='$ram', usbCount=$usbCount, keyboard='$keyboard', display='$display')"
    }
}

调用

val computer= Computer(ram="海力士",keyboard = "双飞燕")

如上代码所示,我们可以通过名称参数来指定修改某个参数,剩下的使用默认参数即可,不得不说确实很强大。

Kotlin中的构建者模式

那么构建者模式在Kotlin中是否真的就成为了一点用都没有的废材了呢?当然不是

需求:

一个对象需要经过多步才能完成构建,而且希望构建的对象是不可变的,这就需要先一步一步的创造构建者,然后在最后一步使用构建者来构建对象。这么说有点抽象,让我们来举个例子。

我们的电脑需要配置CPU和RAM才是一个完整的对象,但是现在CPU与RAM不能同时到位,而是先得到CPU,然后经过很多步骤后才能得到RAM。那么我们就没有办法在得到CPU的时候就把Computer对象创建出来呢?有的同学说了,可以先把对象创建出来,然后等得到了RAM后在此对象上set进去。 这样的话那个Computer对象就成了可变对象了,会存在一些列弊端。如果通过构建者模式实现的话,我们可以多步骤构建builder,等builder准备好了,再调用其build()方法最终创建Computer对象。

使用Java实现Builder模式相信大家已经手到擒来了,那就让我们来研究一下在Kotlin中如何实现Builder模式吧

第一版写法

其实和Java差不多,此处列出几个关键点

  1. Computer添加主private构造函数, 其属性为只读属性,供外部使用
  2. Computer添加第二private构造函数,参数为Builder
    使用Builder来对Computer属性赋值
  3. 添加一内部类Builder,其默认是static的,相当于Java的静态内部类
    Builder类的属性对外只可读,不可设置,设置通过对应的设置方法完成。注意那些设置方法使用了kotlin的apply scope函数。
  4. 在Builder类中添加一个build方法,负责最终构建Computer实例

具体代码如下:

class Computer1 private constructor(
    val cpu: String,
    val ram: String,
    val usbCount: Int,
    val keyboard: String,
    val display: String
) {

    private constructor(builder: Builder) : this(
        builder.cpu,
        builder.ram,
        builder.usbCount,
        builder.keyboard,
        builder.display
    )

    class Builder {
        var cpu: String = ""
            private set
        var ram: String = ""
            private set
        var usbCount: Int = 0
            private set
        var keyboard: String = ""
            private set
        var display: String = ""
            private set

        fun setCup(inputCup: String) = apply {
            this.cpu = inputCup
        }

        fun setRam(inputRam: String) = apply {
            this.ram = inputRam
        }

        fun setUsb(inputUsb: Int) = apply {
            this.usbCount = inputUsb
        }

        fun setKeyboard(inputKeyboard: String) = apply {
            this.keyboard = inputKeyboard
        }

        fun setDisplay(inputDisplay: String) = apply {
            this.display = inputDisplay
        }

        fun build() = Computer1(this)
    }

    override fun toString(): String {
        return "Computer(cpu='$cpu', ram='$ram', usbCount=$usbCount, keyboard='$keyboard', display='$display')"
    }

}

调用

 val computer1 = Computer1.Builder().
                     setCup("英特尔").
                     setRam("金士顿").
                     setDisplay("三星").
                     setUsb(3).
                     setKeyboard("罗技").
                     build()

上面代码中所有参数均是可选的,如果某几个参数是必填的话,只要在Builder的构造函数中加入即可,如下所示

class Computer2 private constructor(... ){
    ...
    class Builder (val cpu: String, val ram: String){
             var usbCount: Int = 0
                  private set
              var keyboard: String = ""
                  private set
              var display: String = ""
                  private set
            ...
    }
}

调用

 val computer2= Computer2.Builder("英特尔","金士顿").
                    setDisplay("京东方").
                    setKeyboard("罗技").
                    build()

第二版写法

上面就是Builder模式在Kotlin中传统的写法。但是Kotlin有一种神奇的类型叫Kotlin Function Literals with Receiver,大概长这样:

T.() -> R

是不是有点懵逼?让我们将概念化繁为简,其直接翻译为:带接收器的函数字面量。由于Lambda是一种函数字面量,所以其可以进一步具体化为:带接收器的Lambda表达式。

例如有如下Lambda

val greet1:()->Unit= { println("hello world")}

我们可以为上面的lambda加上一个String类型的receiver,使其变成下面这样

var greet2: String.() -> Unit = { println("Hello $this") }

我们可以在{}中以this访问这个receiver。值得注意的是,greet2有两种等价的执行方法

greet2("world")
"world".greet2()

利用上述语法可以简化Builder写法,构建出 builder DSL的语法,使其更加紧凑。

我们在Computer类里面加一个companion object,在里面定义一个使用Builder构建对象的方法,其入参为 接收器为Builder的Lambda表达式

class Computer3 private constructor(...){

    companion object {
        inline fun build(block: Builder.() -> Unit) = Builder().apply(block).build()
    }

    class Builder {
        ...
        fun build() = Computer3(this)
    }
}

这块稍微有点难度,要想理解上面的写法,首先需要理解scope函数apply,可以通过阅读我的另一篇文章 秒懂Kotlin之轻松掌握Scope Functions (apply, also,let,run,with)来轻松掌握scope函数。

我们看一下apply函数的定义

public inline fun T.apply(block: T.() -> Unit): T

它的入参是T.() -> Unit 返回类型是T,此处泛型参数T我们传入Builder类,我们就可以使用如下语法调用 inline fun build(block: Builder.() -> Unit) 方法。

调用

 val computer3 = Computer3.build {
         setCup("AMD")
         setRam("海力士")
         setDisplay("三星")
         setUsb(3)
         setKeyboard("双飞燕")
 }

上面的写法仅仅是为Computer类提供了一种简化的调用方式,我们仍然可以使用传统调用方式来使用此构建者。

总结

在kotlin中大部分情况下是不需要Builder模式的,但是某些特殊情形下Builder模式仍然可以大放异彩。

猿猿们,又到了点赞分享的时候了,祝你们圣体健康,生活愉快…

十载求学纵苦三伏三九无悔无怨; 一朝成就再忆全心全力有苦有乐

You May Also Like

About the Author: shusheng007

发表评论

邮箱地址不会被公开。