Kotlin手写建造者模式(Builder)

设计模式系列

前言

我一直以来都认为, 设计模式是前人为解决特定问题而总结出来的方法经验, 尽信书不如无书, 设计模式不必死记硬背, 也不要因为不懂而觉得多么神秘多么高端, 也大可不必因为懂了就觉得多么骄傲多么高人一等.
今天写一个小短篇, 出发点是我工作中遇到一个地方适合Builder模式, 我已经使用kotlin一年多了, 但下意识写出来的还是"长得很Java"的kotlin代码. 兴起之下就想探究一下kotlin下写Builder的最佳实践. 如能帮到读者, 不胜荣幸.

建造者模式 Why?

为什么会产生建造者模式?
这里我借用网友的一个例子, 配电脑.

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

电脑类定义

public class Computer {
    private String cpu;//必须
    private String ram;//必须
    private int usbCount;//可选
    private String keyboard;//可选
    private String display;//可选
}

传统的Java的方式我们怎么来实现呢?
第一种, 用多个构造函数满足不同的配置需求. 所谓折叠构造函数

public class Computer {
     ...
    public Computer(String cpu, String ram) {
        this(cpu, ram, 0);
    }
    public Computer(String cpu, String ram, int usbCount) {
        this(cpu, ram, usbCount, "罗技键盘");
    }
    public Computer(String cpu, String ram, int usbCount, String keyboard) {
        this(cpu, ram, usbCount, keyboard, "三星显示器");
    }
    public Computer(String cpu, String ram, int usbCount, String keyboard, String display) {
        this.cpu = cpu;
        this.ram = ram;
        this.usbCount = usbCount;
        this.keyboard = keyboard;
        this.display = display;
    }
}

显然, 这样的方式非常繁琐, 每增加一个可配置项, 就至少要增加一个构造函数, 而且要修改参数最全的那个构造函数. 当你要使用一个不熟悉的类, 而它采用的是这种方式时, 你就需要搞懂它每一个参数的含义, 避免传错参数, 踩到莫名其妙的坑.

第二种方式, 我给每个可选配置都增加一个setter.

public class Computer {
        ...

    public String getCpu() {
        return cpu;
    }
    public void setCpu(String cpu) {
        this.cpu = cpu;
    }
    public String getRam() {
        return ram;
    }
    public void setRam(String ram) {
        this.ram = ram;
    }
    public int getUsbCount() {
        return usbCount;
    }
...
}

这同样非常繁琐, 当每个配置项都需要配置的时候, 代码行数可能会让你发疯. 除非你老板以代码行数作为KPI.
而且由于每个配置项都开放了修改, 导致一个对象在某个执行点的时候, 状态是不确定的. 你配好的海盗船可能中途被黑心二手商贩换成了别的垃圾内存.

那么, 有没有什么方法能既让我们只设置我们关心的配置, 又不允许后续修改呢?
有的, 标题都告诉你了, 就是Builder

Java版Builder实现

Builder模式需要满足两个需求, 即 只设置我们关心的配置, 且只设置一次

  • 只设置我们关心的配置
    • --> 对于必选配置, 要求使用时必须传入 --> 放到构造函数里
    • --> 对于可选配置, 要求提供设置接口, 调用即可设置
  • 只设置一次 --> 要求配置项只读, 不可写

听起来好像是矛盾的? 其实不然, 只要把上一节两种方式结合一下, 新增一个类来完成组装的工作, 就能同时满足两个需求. 这个新增加的类很像建筑工人, 就称为Builder.
具体怎么写呢? 看代码

public class Computer {
    private final String cpu;//必须
    private final String ram;//必须
    private final int usbCount;//可选
    private final String keyboard;//可选
    private final String display;//可选

    private Computer(Builder builder){
        this.cpu=builder.cpu;
        this.ram=builder.ram;
        this.usbCount=builder.usbCount;
        this.keyboard=builder.keyboard;
        this.display=builder.display;
    }
    //省略getter
    ...
    public static class Builder{
        private String cpu;//必须
        private String ram;//必须
        private int usbCount;//可选
        private String keyboard;//可选
        private String display;//可选
       
        public Builder(String cpu, String ram){
            this.cpu=cpu;
            this.ram=ram;
        }

        public Builder setUsbCount(int usbCount) {
            this.usbCount = usbCount;
            return this;
        }
        public Builder setKeyboard(String keyboard) {
            this.keyboard = keyboard;
            return this;
        }
        public Builder setDisplay(String display) {
            this.display = display;
            return this;
        }        
        public Computer build(){
            return new Computer(this);
        }
    }
}

我们来分析一下这个代码是如何满足上述需求的.
首先, 建立了一个新的静态类Builder, 这个类存有所有Computer需要的参数. Computer里所有参数都是private且为final, 只允许设置一次, 并且构造函数也是private, 以Builder实例为入参, 构造函数中将Builder的参数赋值到Computer的同名参数上. 这就满足了只设置一次的需求.
其次, 在这个Builder类中, 提供所有参数的设置方法, 每个方法都返回Builder自身, 相当于暂存一下配置, 且Builder类的构造函数包含了所有必选配置. 这就满足了所有配置项可配置, 必选配置项一定已配置的需求.
最后, Builder类提供了一个build()方法, 其调用的是Computer的私有构造函数, 并传入自身. 调用build()时才真正生成Computer对象.

有人说, 那我Builder对象在调用build()之前, Builder的配置项也是可以改变的呀, 并没有满足只设置一次的需求.
你说得对, Builder里的配置确实可能被改变, 但我们真正要使用的是Computer对象, Builder对象只是构造Computer对象的一个临时工, 用完就应该抛了. (仿佛在隐喻我自己)
我们使用时只需要链式调用Builder...build()就能生成Computer对象, 如下

Computer computer=new Computer.Builder("因特尔","三星")
        .setDisplay("三星24寸")
        .setKeyboard("罗技")
        .setUsbCount(2)
        .build();

amazing! 简洁, 易懂, 不易出错.

Kotlin版Builder实现

由于Kotlin跟Java的互通性, 你当然可以直接用Kotlin写一遍上述Java版的Builder, 但这就没什么神奇的了. 事实上Kotlin还可以有更优雅的实现方式.

插句题外话, 对于折叠构造函数形式, 可以通过kotlin的命名参数+参数默认值+@JvmOverloads注解, 只需写一个构造函数, 让Kotlin自动生成其他缺省参数的构造函数.

我们知道, Kotlin可以给任意类写扩展函数, 甚至可以给泛型写扩展函数, 就像这样

fun String.hello() { sayHello() }
// 还可以把扩展方法赋给变量
val hello: String.() -> Unit = { sayHello() } // 注意变量的类型声明

于是任意接收者类型的对象都可以调用这个方法. 具体怎么实现的这里我不展开, 有兴趣的同学可以看看kotlin编译后的代码.
这跟建造者模式有什么关系呢?
通过给Builder写扩展函数, 我们可以给建造者模式提供一种新的, 更简洁的写法.
我们可以这么写:

// 省略号的部分跟传统方式一样
class Computer3 private constructor(...){

    companion object {
        inline fun build(block: Builder.() -> Unit) = Builder().apply(block).build() // 注意这里, 将构造过程抽象成一个Builder的扩展函数, 从外部传入, 再由内部调用. 
    }

    class Builder { // 这里也可以不用静态类了
        ... // 这里可以跟传统方式一样, 也可以直接将变量设为public
        fun build() = Computer3(this)
    }
}

于是我们得到了简洁的写法

val computer3 = Computer3.build {
         cpu="AMD"
         ram="海力士"
         display="三星"
         usbCount=3
         keyboard="双飞燕"
 }

看着是不是更符合自然直觉了?
有人要说了, 这也没法要求必选参数啊? 别慌, 必选参数可以加到build()函数上嘛! 作为它的参数就行了.
当然也可以按传统方式调用Builder来构造对象, 随你乐意.

结语

说来说去, Kotlin只是一个更甜的Java, 不要被其花里胡哨的用法迷花了眼, 它只是把脏活累活都藏在了编译器里而已.

好了, 今天就说到这吧, 我是阿黄, 下课!

你可能感兴趣的:(Kotlin手写建造者模式(Builder))