设计模式系列
前言
我一直以来都认为, 设计模式是前人为解决特定问题而总结出来的方法经验, 尽信书不如无书, 设计模式不必死记硬背, 也不要因为不懂而觉得多么神秘多么高端, 也大可不必因为懂了就觉得多么骄傲多么高人一等.
今天写一个小短篇, 出发点是我工作中遇到一个地方适合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, 不要被其花里胡哨的用法迷花了眼, 它只是把脏活累活都藏在了编译器里而已.
好了, 今天就说到这吧, 我是阿黄, 下课!