建造者模式同之前的工厂方法、抽象工厂、单例模式同属创建型模式,关于其的描述是这样的:
建造者模式:是将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
从结果上来讲,和工厂模式一样,都是获得一个对象,但是是有较大区别的,建造者模式更加关注“零件装配”的顺序,而工厂模式及抽象工厂则关注什么产品由什么工厂生产,不关注其内部结构的构建过程。
建造者模式通常包含以下四种角色:
Director:调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。
Builder:给出一个抽象接口,以规范产品对象的各个组成成分的建造。这个接口规定要实现复杂对象的哪些部分的创建,并不涉及具体的对象部件的创建。
ConcreteBuilder:实现Builder接口,针对不同的商业逻辑,具体化复杂对象的各部分的创建。 在建造过程完成后,提供产品的实例。
Product:要创建的复杂对象。
下面通过实例讲解什么是建造者模式,如何通过建造者模式创建一个复杂的对象:
以麦当劳点餐为例,我们通常需要点饮料、主食以及零食,例如果汁+汉堡+薯条这种常见的组合。首先构建一个套餐类:
1. Product
package com.allen.design.builder;
public class Meal {
private String food;
private String drink;
private String snack;
public String getFood() {
return food;
}
public void setFood(String food) {
this.food = food;
}
public String getDrink() {
return drink;
}
public void setDrink(String drink) {
this.drink = drink;
}
public String getSnack() {
return snack;
}
public void setSnack(String snack) {
this.snack = snack;
}
@Override
public String toString() {
return "Meal{" +
"food='" + food + '\'' +
", drink='" + drink + '\'' +
", snack='" + snack + '\'' +
'}';
}
}复制
如果我们想要构建不同的组合以及单点一些项目,例如我们只需要饮料+主食或单点一个主食,通常可以采取多构造器模式:
public Meal(String food){
super();
this.food = food;
}
public Meal(String drink,String food){
super();
this.food = food;
this.drink = drink;
}复制
通过new Meal("香辣鸡腿堡"),new Meal("可乐","香辣鸡腿堡")可以快速构建我们想要的对象,但是,我们并不能直观的知道传入的变量代表什么,也解决不了所有类型的构建,例如构建上述例子中,如果需要单点饮料,就无法做到。
所以,我们需要建造者模式来完成这个构建过程。
2. Builder
抽象建造者对象主要用来定义一个标准的构建过程,虽然这个并不是必须的,但是为了标准讲解建造者模式,我们创建了这个类:
package com.allen.design.builder;
public abstract class Builder {
protected String food;
protected String drink;
protected String snack;
public Builder setFood(String food){
this.food = food;
return this;
}
public Builder setDrink(String drink){
this.drink = drink;
return this;
}
public Builder setSnack(String snack){
this.snack = snack;
return this;
}
public abstract void buildFood();
public abstract void buildDrink();
public abstract void buildSnack();
public abstract Meal build();
}复制
我们看到该类里面有食物、饮料、零食的属性和set方法,以及构建这类属性的抽象方法,接下来我们实现该抽象类。
3. ConcreteBuilder
具体建造者继承了抽象建造者,并且实现了建造的方法和建造的最终结果。
package com.allen.design.builder;
public class MealBuilder extends Builder{
private Meal meal = new Meal();
@Override
public void buildFood() {
meal.setFood(this.food);
}
@Override
public void buildDrink() {
meal.setDrink(this.drink);
}
@Override
public void buildSnack() {
meal.setSnack(this.snack);
}
@Override
public Meal build() {
return meal;
}
}复制
4. Director
指导者,按照指导者的构建顺序进行构建。
package com.allen.design.builder;
public class Director {
public void construct(Builder builder){
builder.buildDrink();
builder.buildFood();
builder.buildSnack();
}
}复制
测试类如下:
package com.allen.design.builder;
public class MainTest {
public static void main(String[] args) {
Builder builder = new MealBuilder().setDrink("橙汁").setFood("麦辣鸡腿堡").setSnack("薯条");
Director director = new Director();
director.construct(builder);
Meal meal = builder.build();
System.out.println(meal);
Builder builder1 = new MealBuilder().setDrink("可乐");
director.construct(builder1);
Meal meal1 = builder1.build();
System.out.println(meal1);
}
}复制
输出结果如下:
为方便理解,我们画出上述四种角色的UML图:
如上便是一个建造者模式的实例,可以看到通过建造者模式,我们可以很方便的生产出各式各样的产品,结合具体一些框架的建造者模式应用,可能我们更容易理解该模式的特点和应用:
OkHttp的建造者模式
OkHttp创建request对象的方法如下:
Request request = new Request.Builder().url("http://www.baidu.com")
.addHeader("Content-Type", "application/json").build();复制
可以看到上述结构和我们的例子中是类似的,打开源码我们看到Request内有个静态内部类:
Builder(Request request) {
this.url = request.url;
this.method = request.method;
this.body = request.body;
this.tag = request.tag;
this.headers = request.headers.newBuilder();
}
public Builder url(HttpUrl url) {
if (url == null) throw new NullPointerException("url == null");
this.url = url;
return this;
}
/**
* Sets the URL target of this request.
*
* @throws IllegalArgumentException if {@code url} is not a valid HTTP or HTTPS URL. Avoid this
* exception by calling {@link HttpUrl#parse}; it returns null for invalid URLs.
*/
public Builder url(String url) {
if (url == null) throw new NullPointerException("url == null");
// Silently replace web socket URLs with HTTP URLs.
if (url.regionMatches(true, 0, "ws:", 0, 3)) {
url = "http:" + url.substring(3);
} else if (url.regionMatches(true, 0, "wss:", 0, 4)) {
url = "https:" + url.substring(4);
}
HttpUrl parsed = HttpUrl.parse(url);
if (parsed == null) throw new IllegalArgumentException("unexpected url: " + url);
return url(parsed);
}
/**
* Sets the URL target of this request.
*
* @throws IllegalArgumentException if the scheme of {@code url} is not {@code http} or {@code
* https}.
*/
public Builder url(URL url) {
if (url == null) throw new NullPointerException("url == null");
HttpUrl parsed = HttpUrl.get(url);
if (parsed == null) throw new IllegalArgumentException("unexpected url: " + url);
return url(parsed);
}
/**
* Sets the header named {@code name} to {@code value}. If this request already has any headers
* with that name, they are all replaced.
*/
public Builder header(String name, String value) {
headers.set(name, value);
return this;
}
/**
* Adds a header with {@code name} and {@code value}. Prefer this method for multiply-valued
* headers like "Cookie".
*
* <p>Note that for some headers including {@code Content-Length} and {@code Content-Encoding},
* OkHttp may replace {@code value} with a header derived from the request body.
*/
public Builder addHeader(String name, String value) {
headers.add(name, value);
return this;
}
/** Removes all headers named {@code name} on this builder. */
public Builder removeHeader(String name) {
headers.removeAll(name);
return this;
}
/** Removes all headers on this builder and adds {@code headers}. */
public Builder headers(Headers headers) {
this.headers = headers.newBuilder();
return this;
}
/**
* Sets this request's {@code Cache-Control} header, replacing any cache control headers already
* present. If {@code cacheControl} doesn't define any directives, this clears this request's
* cache-control headers.
*/
public Builder cacheControl(CacheControl cacheControl) {
String value = cacheControl.toString();
if (value.isEmpty()) return removeHeader("Cache-Control");
return header("Cache-Control", value);
}
public Builder get() {
return method("GET", null);
}
public Builder head() {
return method("HEAD", null);
}
public Builder post(RequestBody body) {
return method("POST", body);
}
public Builder delete(@Nullable RequestBody body) {
return method("DELETE", body);
}
public Builder delete() {
return delete(Util.EMPTY_REQUEST);
}
public Builder put(RequestBody body) {
return method("PUT", body);
}
public Builder patch(RequestBody body) {
return method("PATCH", body);
}
public Builder method(String method, @Nullable RequestBody body) {
if (method == null) throw new NullPointerException("method == null");
if (method.length() == 0) throw new IllegalArgumentException("method.length() == 0");
if (body != null && !HttpMethod.permitsRequestBody(method)) {
throw new IllegalArgumentException("method " + method + " must not have a request body.");
}
if (body == null && HttpMethod.requiresRequestBody(method)) {
throw new IllegalArgumentException("method " + method + " must have a request body.");
}
this.method = method;
this.body = body;
return this;
}
/**
* Attaches {@code tag} to the request. It can be used later to cancel the request. If the tag
* is unspecified or null, the request is canceled by using the request itself as the tag.
*/
public Builder tag(Object tag) {
this.tag = tag;
return this;
}
public Request build() {
if (url == null) throw new IllegalStateException("url == null");
return new Request(this);
}
}复制
可以看到这个是将具体建造者和抽象建造者合二为一,并且使用了静态内部类的方式,简化了代码,也更方便理解,在此基础上,我们对原有例子中代码同样进行改造,改造meal类,增加静态内部类和构造器,如下:
public Meal(){
super();
}
public Meal(Builder builder){
this.drink = builder.drink;
this.food = builder.food;
this.snack = builder.snack;
}
public static class Builder{
String drink;
String food;
String snack;
public Builder setFood(String food){
this.food = food;
return this;
}
public Builder setDrink(String drink){
this.drink = drink;
return this;
}
public Builder setSnack(String snack){
this.snack = snack;
return this;
}
public Meal build(){
Meal meal = new Meal(this);
return meal;
}
}复制
测试类:
Meal meal2 = new Meal.Builder().setDrink("咖啡").build();
Meal meal3 = new Meal.Builder().setDrink("咖啡").setSnack("香芋派").build();
System.out.println(meal2);
System.out.println(meal3);复制
输出结果:
修改后的建造者模式写法更加简单,调用起来也方便的多,想set什么属性就set什么属性。
SWAGGER2的建造者模式
作为当今非常流行的接口描述文档,swagger对rest的描述支持是非常全的,当我们需要发布一个swagger描述时,通常需要生成bean来发布,如下:
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())//调用apiInfo方法,创建一个ApiInfo实例,里面是展示在文档页面信息内容
.select()
//控制暴露出去的路径下的实例
//如果某个接口不想暴露,可以使用以下注解
//@ApiIgnore 这样,该接口就不会暴露在 swagger2 的页面下
.apis(RequestHandlerSelectors.basePackage("com.example"))
.paths(PathSelectors.any())
.build();
}
//构建 api文档的详细信息函数
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
//页面标题
.title("xx接口api")
//创建人
.contact("xxxx")
//版本号
.version("1.0")
//描述
.description("外部系统对接api")
.build();
}复制
其实现也是通过建造者模式,较为复杂,不再详细讲解,有兴趣可以自行翻阅swagger2源码。
总结而来,建造者有以下优缺点:
优点:
1、客户端不需要知道产品内部组成的细节,将产品本身与产品创建的过程解耦,使得相同创建过程可以创建不同的产品对象。
2、每一个具体建造者都相对独立,用户使用不同的具体建造者可以得到不同的产品对象。
3、可以更为精细的控制产品的创建过程。将复杂产品的创建不走分解在不同的方法中,使得创建过程更加清晰,也方便使用程序来控制创建过程
缺点:
如果产品间组成部分差异过大或产品内部变化比较复杂,则不适合使用建造者模式。
所以,建造者模式适合构建有着多个组合部分,但是有统一的构建顺序及规则的对象的构建,例如常见的HttpRequest或RequestEntity构建,或者Datasource、StringBuilder的构建等。
本文通过讲解建造者模式的定义,四个角色,并通过麦当劳点餐实例以及部分应用到该模式的框架分析,来理解建造者模式,并且分析其优缺点,方便以后对相关源码的分析,以及在合适的场合应用建造者模式。