引言
在上一篇Builder模式演义(1)中介绍了Builder模式的标准形式,以及两种基本变换——链式调用和省略指挥者角色。本文将通过分析OkHttp源码阐述Builder模式的另外两种变换——省略抽象Builder角色和Product角色回炉再造。
OkHttp源码中的Builder模式
OkHttp作为开源的Android网络请求框架,以URLConnection和HttpClient的替代者身份出现,名噪江湖。许多开源框架都是基于OkHttp的二次封装,比如OkGo,以及与OkHttp同源的Retrofit。OkHttp的使用非常简单,在Github上OkHttp项目的Wiki/Recipes中有基本的介绍。
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
Response response = client.newCall(request).execute();
//省略其他代码
}
由上述代码的调用风格我们可以基本猜到,Request对象的构建采用了Builder模式。为了验证这一点,我们看看源码中Request类的具体实现。
public final class Request {
final HttpUrl url;
final String method;
final Headers headers;
final RequestBody body;
final Object tag;
private volatile CacheControl cacheControl; // Lazily initialized.
Request(Builder builder) {
//构造函数,省略属性赋值操作
}
public Builder newBuilder() {
return new Builder(this);
}
//省略部分代码
public static class Builder {
HttpUrl url;
String method;
Headers.Builder headers;
RequestBody body;
Object tag;
//省略部分代码
Builder(Request request) {
//构造函数,省略属性赋值操作
}
//省略部分代码
public Request build() {
if (url == null) throw new IllegalStateException("url == null");
return new Request(this);
}
}
上述代码非常清晰,Request类共有6个属性,从名字就可以猜出它们的意义:url代表这个Http请求的url,method指的是POST请求还是GET请求还是其他,header和body自然就是http请求的首部和请求体;tag猜测是用作取消一个请求,cacheControl是缓存控制。Request.Builder中冗余定义了这6个属性。builder在经过一系列的链式调用,对这6个属性中的某几个进行赋值后,最终调用build()方法,生成一个完整的Request对象。build()方法的具体实现也很简单,调用Request的构造方法,将自己作为参数传递过去。Request构造方法内对6个属性一一赋值(没有值时使用默认值)。
省略抽象Builder角色
仔细研究,发现Request类的6个属性分别对应的类型中,Headers、CacheControl、HttpUrl类的内部都含有Builder!RequestBody是个抽象类,其内部没有Builder,但是它的两个子类FormBody和MultipartBody有。于是画出如下UML类图。
对照上一篇 Builder模式演义(1)中GoF标准Builder模式,原本我们很希望RequestBody中有一个Builder作为抽象Builder角色,作为FormBody.Builder和MultipartBody.Builder的共同父类。然而Request.Builder根本不存在!上图中除了RequestBody,其他每个类中的Builder都是独立存在的,除宿主类(Builder所在的外部类),不和其他类发生任何牵扯。
换一种方式理解,如果Builder模式中的ConcreteBuilder只有一个,那么抽象的Builder当然可以省略。此所谓Builder模式变换之 省略抽象Builder角色。
Product角色回炉再造
省略指挥者角色,省略抽象Builder角色,整个Builder模式只剩下两个角色,如下图。
相信你已经非常熟悉Builder模式的使用套路了——在经过一系列的链式调用对属性进行赋值后,ConcreteBuilder最终调用build()方法生成Product对象。一旦调用build()方法,无法再设置或修改属性值了,因为build()返回的是Product类型,而不再是Builder本身。这本身是一种保护机制,也是Builder模式的特性。这好比打包邮寄东西,一旦封包,无法再继续往里面塞,更无法在运输的途中,进行远程遥控替换里面的某件物品。
然而凡事无绝对,设想这样一种场景:假如上述的Request类中的属性数不是6而是30,通过长长的链式调用,我配置了其中的20个属性,一声令下调用build()方法获取到了一个request1对象,并一直在使用着;在某个特殊的场景下,我需要使用和request1基本相同的配置,只有两个属性值不同。这时候该如何去获得request1对象的一个拷贝,然后设置那两个不同的属性值呢?想到两种方式:
- 让Request实现Cloneable接口,或者仿照C++实现一个形如Request(Request other){...}的拷贝构造函数。
- 将request1对象序列化后再反序列化,得到另一个对象request2,它和request1所有属性都相同。
方式1存在着深拷贝、浅拷贝的问题,再者,为每个复杂对象实现Clonable接口或拷贝构造函数工作量巨大而且非常难以维护;方式2存在着空间和性能的开销。Builder模式的问题,有它自己的解决逻辑!从Builder到Product并不一定是单向不可逆的过程。回看文章开头Request类的源码,有一个newBuilder()方法,它返回Request.Builder()类型。newBuilder()的内部,调用的是Builder的一个含Request类型参数的构造函数。
public Builder newBuilder() {
return new Builder(this);
}
Request(Builder builder){...}和Builder(Request request) {...},外部类和内部类的构造函数,是否有种对称美?正是这种美,巧妙地完成了从Product重回Builder的逆向过程。再接下来的事,就是继续链式调用最后调用build()模式一锤定音。至此,Builder模式Builder和Product的关系如下。
OkHttp官网解释回炉再造
其实在Github上OkHttp项目的Wiki/Recipes中,有这样一段话:
Per-call Configuration
All the HTTP client configuration lives in OkHttpClient
including proxy settings, timeouts, and caches. When you need to change the configuration of a single call, call OkHttpClient.newBuilder()
. This returns a builder that shares the same connection pool, dispatcher, and configuration with the original client. In the example below, we make one request with a 500 ms timeout and another with a 3000 ms timeout.
大体意思是,所有HTTP请求都使用全局的OKHttpClient配置,包括代理、超时、缓存等。如果要为某一两个单独的请求修改配置,就调用OkHttpClient.newBuilder()。它返回的OkHttpClient.Builder和原先那个全局的有一样的连接池、分发器和配置。然后就是示例代码,如下。
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay.
.build();
try {
// Copy to customize OkHttp for this request.
OkHttpClient copy = client.newBuilder()
.readTimeout(500, TimeUnit.MILLISECONDS)
.build();
Response response = copy.newCall(request).execute();
System.out.println("Response 1 succeeded: " + response);
} catch (IOException e) {
System.out.println("Response 1 failed: " + e);
}
try {
// Copy to customize OkHttp for this request.
OkHttpClient copy = client.newBuilder()
.readTimeout(3000, TimeUnit.MILLISECONDS)
.build();
Response response = copy.newCall(request).execute();
System.out.println("Response 2 succeeded: " + response);
} catch (IOException e) {
System.out.println("Response 2 failed: " + e);
}
}
对,你没有看错,这不是Request.newBuilder()的使用,而是OKHttpClient.newBuilder()。OKHttpClient类也使用Builer模式!具体的实现请自行查看源码了。
总结
至此,已经介绍了Builder模式的四种变换。
- 链式调用:
并非Builder模式特有,只要在原本返回值为void的方法中返回this,都可以实现链式调用。- 省略指挥者角色:
new builder、链式赋值、最后build,一条龙调用,不再需要指挥者角色。- 省略抽象Builder角色:
具体的Builder只有一个,省略抽象父类。- Product角色的回炉再造:
Product逆转化为Builder,调整某些配置后,重新build,回到Product形态。
Builder设计模式使用如此广泛,又如此灵活。我们在实际开发特别是重构、封装时,可适当借鉴,定能更上一层逼格。有时间可以再挖一挖OkGo和Retrofit中的Builder模式,理解会更加深刻,使用会更加得心应手。