1. 絮絮叨叨
- 一说到如何新建一个对象,熟悉面向对象编程的同学,首先想到的就是
new
一个对象 - 笔者在研究生期间,甚至听到过同学院的男同学这样调侃过找对象的事情:
找对象太难了,僧多粥少,还可能找到个自己不满意的。要是能new一个“对象”多好啊,外貌、性格啥的都是自定义,保证满足要求。要是不满足要求,还能重新set一下属性
- 当时笔者和朋友走在这几个男生的后面,顿时不厚道地大笑出声,虽然我也是单身狗,但这不妨碍我笑啊
1.1 通过构造函数创建对象
-
创建对象,首先想到的就是通过构造函数创建对象,也就是new一个对象,并在new的时候传入初始值
-
考虑具有5个属性的User类,其中name、telephone、sex都是必填字段,否则无法录入信息系统
public class User { private String name; // 必填字段 private String telephone; // 必填字段 private String sex; // 必填字段 private int age; // 可选字段 private String address; // 可选字段 }
-
对于可选字段,我们可以初始化这些字段中任意个,也可以不初始化
-
这时,我们需要有多个构造函数,以创建具有不同状态的user对象
伸缩构造函数问题
- 构造函数个数指数级增长
- 想想在User类中,2个可选字段的存在,就搞得我们需要构建4个构造函数
- 如果有 n n n个可选字段的存在,就需要 2 n 2^n 2n个构造函数
- 构造函数的参数难以对应,容易传参错误
- 构造函数中的参数一旦变多,到底哪个属性对应哪个参数,需要人工确认
- 同时,还相邻的参数还可能类型相同,这就更加容易传错了
- 遇到这样的问题,debug将变得非常困难。因为有些字段的内容可能没有明确的含义,如数值字段,很难判断哪个位置传参了错误
- 那种感觉之酸爽,真的只有经历过的人才知道 😣
- 国外的博客,将这种现象描述为伸缩构造函数问题(
telescoping constructors problem
,中文翻译可能缺少那么点儿味道)
1.2 JavaBean模式构建对象
- 针对伸缩构造函数问题,可以通过提供setter方法设置对象属性值去解决问题
- 也就是JavaBean模式,该模式也存在一些问题:
- 有些字段是必填字段,如果在构建时忘记设置值,后面的调用者必须进行
Preconditions
以保证对象正确初始化 - 同时,因为多要多次调用setter方法设置属性值,构造过程中JavaBean可能处于不一致的状态
- 联系单例的初始化,已经成功创建的对象,还需要通过setter方法初始化(之前的博客:层层递进,实现单例模式)
- 这样的话,不同线程获得的单例对象状态是不一致的
public static Singleton getInstance() { if (instance == null) { // 第一重检验,避免不必要的同步 synchronized (Singleton.class) { // 同步锁 if (instance == null) { // 第二重检验,避免重复创建 instance = new Singleton(); // 至此,单例对象已成功创建,但其内容未初始化 // ...,通过setter方法设置属性值 } } } return instance; }
- setter方法的开放,彻底打破了构建不可变类的可能
- 有些字段是必填字段,如果在构建时忘记设置值,后面的调用者必须进行
知识拓展: 不可变类(Immutable Class
)
- 大名鼎鼎的不可变类,当属Java中自带的String类(String类为什么不可变?)
- 开发中常用的Google guava包中,就提供了很多不可变的容器类,如ImmutableMap、ImmutableList、ImmutableSet等
- 不可变类的的定义非常严格,想要学习如何构建不可变类,可以参考博客:How to create immutable class in Java
2. 神奇的Builder模式
-
针对上面的问题,可以使用建造者(Builder)模式,分离对象的属性与创建过程
-
如果有幸阅读一些开源代码,你会经常碰到Builder模式,只是不知道这就是Builde者模式而已
RuleIndex ruleIndex = RuleIndex.builder() .register(projectRule1) .register(projectRule2) .register(filterRule) .register(anyRule) .build();
-
通过上面的代码示例,我们不难发现
- 如果使用构造函数创建对象,必须通过参数传入属性值,属性与创建过程是绑定在一起的
- 如果使用Builder模式,建造者自动实现复杂的对象创建过程。调用者只需要指定一些重要属性,便可以得到一个创建好的对象
- 对于调用者来说,在Builder模式中,对象的属性与创建过程分离了
2.1 使用Builder模式重构User类
2.1.1 代码实现
-
简化后的Builder模式重写User类:
import com.google.common.base.Preconditions; import org.apache.commons.lang.StringUtils; public class User { private String name; // 必填字段 private String telephone; // 必填字段 private String sex; // 必填字段 private int age; // 可选字段 private String address; // 可选字段 private User(UserBuilder builder) { this.name = builder.name; this.telephone = builder.telephone; this.sex = builder.sex; this.age = builder.age; this.address = builder.address; } @Override public String toString() { return "{\"name\":" + name + ",\"telephone\":\"" + telephone + "\",\"sex\":\"" + sex + "\",\"age\":\"" + age + "\",\"address\":" + address + "}"; } public static class UserBuilder { private String name; private String telephone; private String sex; private int age; private String address; public UserBuilder setName(String name) { this.name = name; return this; } public UserBuilder setTelephone(String telephone) { this.telephone = telephone; return this; } public UserBuilder setSex(String sex) { this.sex = sex; return this; } public UserBuilder setAge(int age) { this.age = age; return this; } public UserBuilder setAddress(String address) { this.address = address; return this; } public User build() { // 必填字段校验 Preconditions.checkArgument(StringUtils.isNotBlank(name), "name can not be empty"); Preconditions.checkArgument(StringUtils.isNotBlank(telephone), "name can not be empty"); Preconditions.checkArgument(StringUtils.isNotBlank(telephone), "name can not be empty"); // 传入builder对象以构建user对象 User user = new User(this); System.out.println("通过Builder模式成功构建User对象"); return user; } } }
-
在main方法中,通过Builder模式创建User对象
public static void main(String[] args) { User user = new User.UserBuilder() .setName("张三") .setTelephone("18812546788") .setSex("男") .setAge(31) .build(); System.out.println("通过Builder模式构建的User对象: " + user); }
-
执行结果如下:
2.1.2 总结与反思
(1)简化版的Builder模式总结
- 在User类中创建一个静态内部类Builder,该Builder类复制了User类的所有属性
- User类中创,创建一个以Builder对象为参数的
private
构造函数,从而可以通过Builder对象初始化User对象 - Builder类中,提供了设置属性的setter方法
(1)与返回void的常见setter方法不同,Builder类中的setter方法会返回Builder类型的对象
(2)这样可以基于Builder对象不断进行属性设置,还可以灵活地调整构建过程(谁先谁后,是否设置某个属性)
(3)写到这里,忽然有点明白下面这句话的含义了将复杂对象的构建拆分成多个简单对象的构建,通过调整构建步骤可以创建具有不同表示的对象。
- Builder类中,创建一个build()方法:先进行必填字段的校验,然后调用User的private构造函数,完成user对象的创建并返回
(2)为什么User类中的构造函数是private?
- 最开始,通过Builder模式重构User类时,自己将User类的构造函数设置为
public
权限 - 后续学习过程,阅读的Builder模式源码变多后,发现目标类的构造函数都是
private
权限 - 假设使用public权限,可以跳过build()方法,直接将未设置任何属性的Builder对象作为User类构造函数的入参。
- 最终,创建的user对象将不符合要求,因为必填字段未初始化
User user = new User(new User.UserBuilder());
- 设置为private权限,保证只能通过Builder类的build()方法完成合法的use对象的创建
(3)关于Builder作为静态内部类的说明
- 除了可以将Builder类作为静态内部类,还可以将Buidler类抽象成一个外部类
- 例如,mybatis的
SqlSessionFactoryBuilder
、Google LocalCache中的CacheBuilder
,都是外部类
2.2 静态的builder()创建Builder对象
-
细心的读者可能会发现,main方法中通过Builder模式构建user对象的代码,与示例的开源代码有一些出入
-
开源代码直接通过
RuleIndex.builder()
方法创建Builder对象,而非通过new User.UserBuilder()
方式 -
其实,builder()方法是一个静态方法,其内部实现就是创建一个Builder对象并返回
-
基于User类示例,增加一个静态的builder()方法。main方法中,通过Builder模式构建user对象的代码便可以与示例的开源代码一致
public static UserBuilder builder(){ return new UserBuilder(); } public static void main(String[] args) { User user = User.builder() .setName("张三") .setTelephone("18812546788") .setSex("男") .setAge(31) .build(); System.out.println("通过Builder模式构建的user对象: " + user); }
build()方法 vs builder()方法
- 位置不同:build()方法位于Builder类中,builder()方法位于User类中
- 作用不同:build()方法,用于创建user对象;builder()方法,用于创建builder对象,与
new User.UserBuilder()
方式等价
2.3 另一种对象创建方式
-
到目前为止,示例代码都是通过传入Builder对象实现目标对象的构建
-
其实,在很多开源代码中,还有另一种创建目标对象的方法:直接传入Builder对象的属性值,以创建目标对象
-
例如,Google guava的
ImmutableList.Builder
类,其build()方法如下。 -
它并未将Builder对象作为ImmutableList类的构造函数入参,而是传入具体的属性值实现创建
@Override public ImmutableList<E> build() { forceCopy = true; return asImmutableList(contents, size); } static <E> ImmutableList<E> asImmutableList(Object[] elements, int length) { switch (length) { case 0: return of(); case 1: return of((E) elements[0]); default: if (length < elements.length) { elements = Arrays.copyOf(elements, length); } return new RegularImmutableList<E>(elements); } }
一些说明
- 其他博客的理解:适用于属性之间关联不多且大量属性都有默认值的场景。
- 自己的理解:
- 这样的创建方式,适合需要通过Builder对象初始化的属性较少的情况
- 反过来思考:如果属性较少,Builder模式其实显得没有意义,完全可以使用构造函数创建对象
- 更准确地说,这样的创建方式,适合属性类型为数组或容器类的情况
- 这样可以通过类似put、add等方法,不断向容器中添加元素
- 两种创建方式对比如下:
应用场景
-
应用一:JDK中StringBuilder和StringBuffer,就是通过append方法不断向存储字符串的
char[]
中添加字符,然后通过toString()方法直接创建一个String对象 -
应用二:MyBatis的
SqlSessionFactoryBuilder
类,其关键代码如下:public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { SqlSessionFactory var5; try { // 构建XMLConfigBuilder对象 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // 调用XMLConfigBuilder对象的parse()方法构建Configuration对象 var5 = this.build(parser.parse()); } catch (Exception var14) { throw ExceptionFactory.wrapException("Error building SqlSession.", var14); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException var13) { } } return var5; } // 最终调用如下build方法,实现SqlSessionFactory的构建 public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); // config对象直接作为构造函数的入参 }
3. 传统的Builder模式
3.1 UML图
- 上面的Builder模式是改良过的版本,传统的Builder模式更加复杂
- 产品(Product):包含多个组成部件(属性)的复杂对象,是Builder最终要创建的目标对象
- 抽象建造者(Builder):一般为抽象类,定义了创建产品各组成部件的抽象方法,即设置对象属性的抽象方法);通常还包含一个返回产品的getResult()方法,对应改良版本的build()方法
- 具体创建者(Concrete Builder):继承抽象创建者,实现创建产品各组成部件的抽象方法;也就是说,具体创建者负责创建具体的产品
- 指挥者(Director):包含一个contruct()方法,通过调用Builder中创建对象各组成部件的方法和getResult()方法,实现复杂对象的创建。对应改良版本中,main()方法通过Builder创建产品的代码
3.2 传统Builder模式编程实战
-
需求:
- 电脑的几大关键组成部分:CPU、内存、显示器、磁盘、键盘、鼠标
- 不同型号的电脑,其配置也有所差异
- 可以通过Builder模式,将电脑的组成部件和创建过程分离
-
定义电脑类,对应模式中的产品
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; public class Computer { private String cpu; private String memory; private String disk; private String display; private String keyboard; private String mouse; // 省略getter和setter方法 @Override public String toString() { return JSON.toJSONString(this, SerializerFeature.WRITE_MAP_NULL_FEATURES); } }
-
定义抽象构建者类
public abstract class ComputerBuilder { // 创建产品对象 protected Computer computer = new Computer(); // 创建产品对象的各组成部件,即设置对象的属性 public abstract ComputerBuilder setCpu(); public abstract ComputerBuilder setMemory(); public abstract ComputerBuilder setDisk(); public abstract ComputerBuilder setDisplay(); public abstract ComputerBuilder setKeyboard(); public abstract ComputerBuilder setMouse(); // 返回产品对象 public Computer getComputer(){ return computer; } }
-
定义两个具体构建者类,分别用于构建家用电脑和实验室电脑
public class GeneralComputerBuilder extends ComputerBuilder { @Override public ComputerBuilder setCpu() { computer.setCpu("intel i5"); return this; } @Override public ComputerBuilder setMemory() { computer.setMemory("8GB"); return this; } @Override public ComputerBuilder setDisk() { computer.setDisk("1TB"); return this; } @Override public ComputerBuilder setDisplay() { computer.setDisplay("联想显示器"); return this; } @Override public ComputerBuilder setKeyboard() { computer.setKeyboard("戴尔键盘"); return this; } @Override public ComputerBuilder setMouse() { computer.setMouse("戴尔鼠标"); return this; } } public class LabComputerBuilder extends ComputerBuilder{ @Override public ComputerBuilder setCpu() { computer.setCpu("intel gold"); return this; } @Override public ComputerBuilder setMemory() { computer.setMemory("16GB"); return this; } @Override public ComputerBuilder setDisk() { computer.setDisk("2TB"); return this; } @Override public ComputerBuilder setDisplay() { computer.setDisplay("三星2k曲面屏"); return this; } @Override public ComputerBuilder setKeyboard() { computer.setKeyboard("罗技游戏键盘"); return this; } @Override public ComputerBuilder setMouse() { computer.setMouse("罗技无线蓝牙鼠标"); return this; } }
-
定义指挥者,通过构建者类实现电脑的创建
public class Director { private ComputerBuilder builder; public Director(ComputerBuilder builder) { this.builder = builder; } public Computer construct() { // 调用builder的属性设置方法 builder.setCpu(); builder.setMemory(); builder.setDisk(); builder.setDisplay(); builder.setKeyboard(); builder.setMouse(); // 返回生产好的电脑 return builder.getComputer(); } }
-
在main方法中,使用Builder模式创建电脑
public static void main(String[] args) { GeneralComputerBuilder builder = new GeneralComputerBuilder(); Director director = new Director(builder); Computer computer = director.construct(); // 查看电脑信息 System.out.println(computer.getClass().getSimpleName() + ": " + computer); }
3.3 回头看:Builder模式
Builder模式的定义
- 《设计模式》一书对Buidler模式的定义:
- 将复杂对象的创建过程与它的表示分离,使得同样的创建步骤可以创建出不同的表示
自己的理解:对象的创建与属性的设置是分离的
- Builer模式以正确的动作顺序,一步一步创建复杂对象
- 指挥者掌控对象的创建过程,且只需要知道待创建对象的类型
- 将复杂对象的创建过程与它的表示分离,使得同样的创建步骤可以创建出不同的表示
- Builder就像一个Fluent Interface,可以像类似lambda表达式一样,以方法级联或者方法链的方式创建对象
这是Builder模式改良后的效果
Builder模式 vs 工厂模式
-
二者都用于创建对象,它们者最大的区别:Builder模式为对象的创建过程提供了更多的控制权
其实,就是提供了设置对象属性的权力,而非什么都不能控制,最终获得一个不满意的产品
-
工厂模式专注于What:创建一个指定类型的固定对象,Builder专注于How:以什么样的步骤创建同一类型、但表示(属性)不同的对象
In one sentence, abstract factory pattern is the answer to “WHAT” and the builder pattern to “HOW”.
改良版本的优缺点
- 优点
- 很好地解决了使用构造函数创建对象时的伸缩构造函数问题,以及使用Java Bean模式创建对象时的状态不一致、破坏不可变特性的问题
- 对象的创建过程更加灵活:ImmutableList.Builder不同的add顺序,最终创建ImmutableList也会有所不同;可以选择性的初始化对象的某些属性,而非所有属性
- 在笔者看来,相对通过构造函数创建对象,代码可读性更高。起码能将属性和值关联起来,或者能清晰地知道对象的内容(如ImmutableList)
- 缺点
- 一旦目标类的属性发生变化,Builder类也需要同步修改
传统版本的优缺点(自己仍需好好理解)
- 优点
- 封装性好,实现了对象的创建与表示分离
- 扩展性好,具体建造者之间相互独立,有利于系统的解耦
- 客户端无需知道产品的内部组成细节,建造者可以创建具有个性化的产品,而不影响其他模块
- 缺点:
- 产品的组成部分必须相同,也就说Builder模式只能生产具有不同细节的同一类产品
- 如果产品的组成部件发生变化,建造者也要同步修改,不易进行代码维护
4. 从源码学习Builder模式
4.1 OkHttpClient
4.1.1 OkHttpClient中Builder模式的实现逻辑
-
OkHttpClient是典型的Builder模式,使用Builder对象作为其构造函数的入参
public OkHttpClient() { this(new Builder()); } OkHttpClient(Builder builder) { this.dispatcher = builder.dispatcher; this.proxy = builder.proxy; ... // 省略其他初始代码,OkHttpClient属性的初始化,都是由Builder中的属性值决定 }
-
其中,Builder构造函数为OkHttpClient的各种属性赋予了默认值
public Builder() { dispatcher = new Dispatcher(); ... // 其他属性省略,只展示部分属性的默认值 connectTimeout = 10_000; readTimeout = 10_000; writeTimeout = 10_000; pingInterval = 0; }
-
Builder类支持通过以属性名为方法名的setter方法,例如readTimeout()可以设置readTimeout属性的值,并返回Builder对象
public Builder readTimeout(long timeout, TimeUnit unit) { readTimeout = checkDuration("timeout", timeout, unit); return this; }
-
Builder类的build()方法,直接调用
OkHttpClient(Builder builder)
实现OkHttpClient的创建public OkHttpClient build() { return new OkHttpClient(this); }
4.1.2 如何创建OkHttpClient?
-
方法一: 通过
new OkHttpClient.Builder()
创建自定义属性的OkHttpClientprivate final OkHttpClient httpClient = new OkHttpClient.Builder() .addInterceptor(userAgent(DRIVER_NAME + "/" + DRIVER_VERSION)) .readTimeout(5_000, TimeUnit.MILLISECONDS) .socketFactory(new SocketChannelSocketFactory()) .build();
-
方法二: 通过
new OkHttpClient()
创建具有默认属性值的OkHttpClientpublic final OkHttpClient client = new OkHttpClient();
5. 菜鸟的总结
- 说实话,看了这么Builder模式的源码,还从未见过使用复杂的传统Builder模式的
- 改良的Builder模式的实现方式大同小异
- Builder对象的创建:直接创建,通过静态的builder()方法创建
- 目标对象的创建(特指new那一步):
- 以Builder对象作为构造函数的入参,如
OkHttpClient
- 或者以Builder对象的属性、或者处理后的属性作为构造函数的入参
- 以Builder对象作为构造函数的入参,如
- 自己对Builder模式的理解:
- 工厂模式中,client获得的是已经创建好的、具有默认值的对象;Builder模式中,client可以掌控对象的构建过程(例如,集中添加元素时,谁先谁后;创建复杂对象时,是否设置某些属性等),从而获得已经创建好的、具有自定义值的对象
- Buidler模式解决了通过构造函数、Java Bean模式创建对象存在的问题
- 一句话来说,虽然实现Builder模式增加了开发工作量,但是client可以轻松获得已经创建好的、具有自定义属性的对象
- Builder模式在JDK源码、各种类库或开源组件的应用场景很多,但自己好像没办法跟工作中的问题联系起来
- 可能是自己的理解不够透彻,也可能是工作中尚未出现需要使用Builder模式的场景😂
参考链接:
- 最好的参考资料,有着作者自己的见解:Builder Design Pattern
- 快速上手Builder模式代码开发:Builder模式
- 最简单的讲解: Java Builder 模式,你搞懂了么?
- 晦涩难懂的讲解:建造者模式(Bulider模式)详解
- 下一个目标,自定义@Builder注解:Java Builder模式的写法和lombok插件@Builder注解的支持