美文网首页
Kotlin手写建造者模式(Builder)

Kotlin手写建造者模式(Builder)

作者: hjhjw1991 | 来源:发表于2022-10-09 21:19 被阅读0次

    设计模式系列

    前言

    我一直以来都认为, 设计模式是前人为解决特定问题而总结出来的方法经验, 尽信书不如无书, 设计模式不必死记硬背, 也不要因为不懂而觉得多么神秘多么高端, 也大可不必因为懂了就觉得多么骄傲多么高人一等.
    今天写一个小短篇, 出发点是我工作中遇到一个地方适合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)

          本文链接:https://www.haomeiwen.com/subject/avgoartx.html