前言:最近闲来无事的时候想着看看一些平常用的三方库源码,没想到看了之后才知道直接撸源码好伤身体,一般设计优秀的开源库都会涉及很多的设计模式,就比如 android 开发使用频繁的 okHttp 打开源码一看,纳尼?Builder 模式随处可见,于是乎,这篇文章就来对 Builder 模式进行一个简单总结,主要针对便于分析 android 相关源码,以实际应用出发~
在 oop 编码设计中,我们有句经典的话叫做 "万物皆对象".实际开发中,我们只要能拿到类的实例,即对象。就可以开始搞事情啦,可以命令对象去做一些事情,当然啦~每个对象的能力都是不同的,能做的事情也是不同。对象中存储着类的成员属性(成员变量和成员方法)。我们命令对象去为我们工作,其实就是调用对象特有的属性。刚刚我们也说了,每个对象的能力是不同的,对象所能做的事情,在一开始被创建的时候就决定了。下面先来说一下对象的构建方法。
假设一个场景:我们用一个class来表示车,车有一些必需的属性,比如:车身,轮胎,发动机,方向盘等。也有一些可选属性,假设超过10个,比如:车上的一些装饰,安全气囊等等非常多的属性。
如果我们用构造器来构造对象,我们的做法是 提供第一个包含4个必需属性的构造器,接下来再按可选属性依次重载不同的构造器,这样是可行的,但是会有以下一些问题:
1public class Car { 2 /** 3 * 必需属性 4 */ 5 private String carBody;//车身 6 private String tyre;//轮胎 7 private String engine;//发动机 8 private String aimingCircle;//方向盘 9 /** 10 * 可选属性 11 */ 12 private String decoration;//车内装饰品 13 14 /** 15 * 必需属性构造器 16 * 17 * @param carBody 18 * @param tyre 19 * @param engine 20 */ 21 public Car(String carBody, String tyre, String engine) { 22 this.carBody = carBody; 23 this.tyre = tyre; 24 this.engine = engine; 25 } 26 27 /** 28 * 假如我们需要再添加车内装饰品,即在原来构造器基础上再重载一个构造器 29 * 30 * @param carBody 31 * @param tyre 32 * @param engine 33 * @param aimingCircle 34 * @param decoration 35 */ 36 public Car(String carBody, String tyre, String engine, String aimingCircle, String decoration) { 37 this.carBody = carBody; 38 this.tyre = tyre; 39 this.engine = engine; 40 this.aimingCircle = aimingCircle; 41 this.decoration = decoration; 42 } 43} 复制代码
提供无参的构造函数,暴露一些公共的方法让用户自己去设置对象属性,这种方法较之第一种似乎增强了灵活度,用户可以根据自己的需要随意去设置属性。但是这种方法自身存在严重的缺点:
因为构造过程被分到了几个调用中,在构造中 JavaBean 可能处于不一致的状态。类无法仅仅通过判断构造器参数的有效性来保证一致性。还有一个严重的弊端是,JavaBeans 模式阻止了把类做成不可变的可能。,这就需要我们付出额外的操作来保证它的线程安全。
1public class Car { 2 /** 3 * 必需属性 4 */ 5 private String carBody;//车身 6 private String tyre;//轮胎 7 private String engine;//发动机 8 private String aimingCircle;//方向盘 9 /** 10 * 可选属性 11 */ 12 private String decoration;//车内装饰品 13 14 public void setCarBody(String carBody) { 15 this.carBody = carBody; 16 } 17 18 public void setTyre(String tyre) { 19 this.tyre = tyre; 20 } 21 22 public void setEngine(String engine) { 23 this.engine = engine; 24 } 25 26 public void setAimingCircle(String aimingCircle) { 27 this.aimingCircle = aimingCircle; 28 } 29 30 public void setDecoration(String decoration) { 31 this.decoration = decoration; 32 } 33} 复制代码
那么有没有什么方法可以解决以上问题呢?当然有啦~下面我们的主角上场-----Builder 模式
我们用户一般不会自己来完成 car 组装这些繁琐的过程,而是把它交给汽车制造商。由汽车制造商去完成汽车的组装过程,这里的 Builder 就是汽车制造商,我们的 car 的创建都交由他来完成,我们只管开车就是啦, 先来个代码实际体验一下~
1public final class Car { 2 /** 3 * 必需属性 4 */ 5 final String carBody;//车身 6 final String tyre;//轮胎 7 final String engine;//发动机 8 final String aimingCircle;//方向盘 9 final String safetyBelt;//安全带 10 /** 11 * 可选属性 12 */ 13 final String decoration;//车内装饰品 14 /** 15 * car 的构造器 持有 Builder,将builder制造的组件赋值给 car 完成构建 16 * @param builder 17 */ 18 public Car(Builder builder) { 19 this.carBody = builder.carBody; 20 this.tyre = builder.tyre; 21 this.engine = builder.engine; 22 this.aimingCircle = builder.aimingCircle; 23 this.decoration = builder.decoration; 24 this.safetyBelt = builder.safetyBelt; 25 } 26 ...省略一些get方法 27 public static final class Builder { 28 String carBody; 29 String tyre; 30 String engine; 31 String aimingCircle; 32 String decoration; 33 String safetyBelt; 34 35 public Builder() { 36 this.carBody = "宝马"; 37 this.tyre = "宝马"; 38 this.engine = "宝马"; 39 this.aimingCircle = "宝马"; 40 this.decoration = "宝马"; 41 } 42 /** 43 * 实际属性配置方法 44 * @param carBody 45 * @return 46 */ 47 public Builder carBody(String carBody) { 48 this.carBody = carBody; 49 return this; 50 } 51 52 public Builder tyre(String tyre) { 53 this.tyre = tyre; 54 return this; 55 } 56 public Builder safetyBelt(String safetyBelt) { 57 if (safetyBelt == null) throw new NullPointerException("没系安全带,你开个毛车啊"); 58 this.safetyBelt = safetyBelt; 59 return this; 60 } 61 public Builder engine(String engine) { 62 this.engine = engine; 63 return this; 64 } 65 66 public Builder aimingCircle(String aimingCircle) { 67 this.aimingCircle = aimingCircle; 68 return this; 69 } 70 71 public Builder decoration(String decoration) { 72 this.decoration = decoration; 73 return this; 74 } 75 /** 76 * 最后创造出实体car 77 * @return 78 */ 79 public Car build() { 80 return new Car(this); 81 } 82 } 83} 复制代码
现在我们的类就写好了,我们调用的时候执行一下代码:
1 Car car = new Car.Builder() 2 .build(); 复制代码
打断点,debug运行看看效果:
可以看到,我们默认的 car 已经制造出来了,默认的零件都是 "宝马",滴滴滴~来不及解释了,快上车。假如我们不使用默认值,需要自己定制的话,非常简单。只需要拿到 Builder 对象之后,依次调用指定方法,最后再调用 build 返回 car 即可。下面代码示例:
1 //配置car的车身为 奔驰 2 Car car = new Car.Builder() 3 .carBody("奔驰") 4 .build(); 复制代码
依旧 debug 看看 car 是否定制成功~
咦,神奇的定制 car 定制成功了,话不多说,继续开车~~
我们在 Builder 类中的一系列构建方法中还可以加入一些我们对配置属性的限制。例如我们给 car 添加一个安全带属性,在 Buidler 对应方法出添加以下代码:
1 public Builder safetyBelt(String safetyBelt) { 2 if (safetyBelt == null) throw new NullPointerException("没系安全带,你开个毛车啊"); 3 this.safetyBelt = safetyBelt; 4 return this; 5 } 复制代码
然后调用的时候:
1 //配置car的车身为 奔驰 2 Car car = new Car.Builder() 3 .carBody("奔驰") 4 .safetyBelt(null) 5 .build(); 复制代码
我们给配置安全带属性加了 null 判断,一但配置了null 属性,即会抛出异常。好了 car 构建好了,我们来开车看看~
依旧 debug 开车走起~
bom~~~不出意外,翻车了。。。
最后有客户说了,你制造出来的 car 体验不是很好,想把车再改造改造,可是车已经出厂了还能改造吗?那这应该怎么办呢?不要急,好说好说,我们只要能再拿到 Builder 对象就有办法。下面我们给 Builder 添加如下构造,再对比下 Car 的构造看看有啥奇特之处:
1 /** 2 * 回厂重造 3 * @param car 4 */ 5 public Builder(Car car) { 6 this.carBody = car.carBody; 7 this.safetyBelt = car.safetyBelt; 8 this.decoration = car.decoration; 9 this.tyre = car.tyre; 10 this.aimingCircle = car.aimingCircle; 11 this.engine = car.engine; 12 } 13 14 /** 15 * car 的构造器 持有 Builder,将 builder 制造的组件赋值给 car 完成构建 16 * 17 * @param builder 18 */ 19 public Car(Builder builder) { 20 this.carBody = builder.carBody; 21 this.tyre = builder.tyre; 22 this.engine = builder.engine; 23 this.aimingCircle = builder.aimingCircle; 24 this.decoration = builder.decoration; 25 this.safetyBelt = builder.safetyBelt; 26 } 复制代码
咦,似乎有着对称的关系,没错。我们提供对应的构造。调用返回对应的对象,可以实现返回的效果。在 Car 中添加方法
1 /** 2 * 重新拿回builder 去改造car 3 * @return 4 */ 5 public Builder newBuilder() { 6 return new Builder(this); 7 } 复制代码
现在来试试能不能返厂重建?把原来的宝马车重造成奔驰车,调用代码:
1Car newCar = car.newBuilder() 2 .carBody("奔驰") 3 .safetyBelt("奔驰") 4 .tyre("奔驰") 5 .aimingCircle("奔驰") 6 .decoration("奔驰") 7 .engine("奔驰") 8 .build(); 复制代码
行,车改造好了,我们继续 debug ,试试改造完满不满意
哈哈,已经改造好了,客户相当满意~~
下面分析一下具体是怎么构建的。
至此,我们的 Builder 模式体验就结束了,这里讲的只是 Builder 模式的一个变种,即在 android 中应用较为广泛的模式,下面总结一下优缺点:
解耦,逻辑清晰。统一交由 Builder 类构造,Car 类不用关心内部实现细节,只注重结果。
链式调用,使用灵活,易于扩展。相对于方法一中的构造器方法,配置对象属性灵活度大大提高,支持链式调用使得逻辑清晰不少,而且我们需要扩展的时候,也只需要添加对应扩展属性即可,十分方便。
解决方法:不会偷懒的程序猿不是好程序猿,针对以上缺点,IDEA 系列的 ide ,有相应的插件 InnerBuilder
可以自动生成 builder 相关代码,安装自行 google,使用的时候只需要在实体类中 alt + insert 键,会有个 build 按钮提供代码生成。
使用场景
一般如果类属性在4个以上的话,建议使用 此模式。还有如果类属性存在不确定性,可能以后还会新增属性时使用,便于扩展。
1. 在 okHttp 中广泛使用
开篇我们也说到了 Builder 模式在 okHttp 中随处可见。比如在OkHttpClient,Request,Response 等类都使用了此模式。下面以
Request 类为例简要说明,具体的可以去下载源码查看,按照上面的套路基本没问题。
Request 有6个属性,按照套路 构造方法持有一个 Builder ,在构造中将 builder 制造的组件赋值给 Request 完成构建,提供 newBuilder 用于重新获得 Builder 返厂重建:
1final HttpUrl url; 2 final String method; 3 final Headers headers; 4 final RequestBody body; 5 final Object tag; 6 7 private volatile CacheControl cacheControl; // Lazily initialized. 8 9 Request(Builder builder) { 10 this.url = builder.url; 11 this.method = builder.method; 12 this.headers = builder.headers.build(); 13 this.body = builder.body; 14 this.tag = builder.tag != null ? builder.tag : this; 15 } 16 17 public Builder newBuilder() { 18 return new Builder(this); 19 } 复制代码
Builder 有两个构造,第一个空构造中初始化两个默认值。第二个构造持有 Request 用于重新构建 Builder 返厂重建。
1public Builder() { 2 this.method = "GET"; 3 this.headers = new Headers.Builder(); 4 } 5 6 Builder(Request request) { 7 this.url = request.url; 8 this.method = request.method; 9 this.body = request.body; 10 this.tag = request.tag; 11 this.headers = request.headers.newBuilder(); 12 } 复制代码
剩下的就是一些属性初始化的方法,返回值为 Builder 方便链式调用。这里就列出一个方法,详细的请查看源码,最后调用 build() 方法 初始化 Request 传入 Builder 完成构建。
1 public Builder url(HttpUrl url) { 2 if (url == null) throw new NullPointerException("url == null"); 3 this.url = url; 4 return this; 5 } 6...此处省略部分方法 7 public Request build() { 8 if (url == null) throw new IllegalStateException("url == null"); 9 return new Request(this); 10 } 复制代码
2、在 android 源码中 AlertDialog 使用
在 AlertDialog 中使用到的 Builder 模式也是这种套路,我相信如果前面理解了,自己去看看源码应该是手到擒来的事。由于篇幅原因,在这里就不展开了。
结语:个人觉得 对于设计模式的学习是相当有必要的,有时候我们需要去读一下常用开源框架的源码,不仅可以从中学习到一些设计思想,还可以方便日常使用。在一篇博客上面看到这句话 " 我们不重复造轮子不表示我们不需要知道轮子该怎么造及如何更好的造! ",而设计模式便是读懂框架源码的基石,因为往往优秀的框架都会涉及很多设计模式。后面本人也会不断更新,不断学习新的设计模式,进而总结出来~
更多原创文章会在公众号第一时间推送,欢迎扫码关注 张少林同学