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, 不要被其花里胡哨的用法迷花了眼, 它只是把脏活累活都藏在了编译器里而已.

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,104评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,816评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,697评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,836评论 1 298
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,851评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,441评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,992评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,899评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,457评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,529评论 3 341
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,664评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,346评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,025评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,511评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,611评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,081评论 3 377
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,675评论 2 359

推荐阅读更多精彩内容

  • 本文,我们来学习另外一个比较常用的创建型设计模式,Builder 模式,中文翻译为建造者模式或者构建者模式,也有人...
    舍是境界阅读 274评论 0 1
  • 建造者模式的原理和代码实现非常简单,掌握起来并不难,难点在于应用场景。 比如,你有没有考虑过这样几个问题:直接使用...
    vannesspeng阅读 247评论 0 1
  • 一句话概括:私有化类的构造函数,并在类的内部添加用于实例化该类的建造者内部类。 当对象包含很多属性时,可以引入Bu...
    智行孙阅读 640评论 1 0
  • PS:转载请注明出处作者: TigerChain地址: http://www.jianshu.com/p/300c...
    TigerChain阅读 1,713评论 1 18
  • 1.介绍 允许用户在不知道内部构建细节的情况下,可以更精细地控制对象的构造流程。为了将构建复杂对象的过程和它的部件...
    小菜_charry阅读 213评论 0 0