本文主要介绍建造者模式的原理、使用及特性。



一、前言

最近在接触OkHttp源码的过程中看到很多设计模式的优秀应用范例。建造者模式在其中有着广泛的应用,其核心类如OkHttpClient、Request以及Response都使用了建造者模式,可见建造者模式在OkHttp的设计中起着非常重要的作用。

那么,建造者模式是什么?为什么OkHttp在设计中如此广泛的使用该模式,使用该模式能解决什么问题?有什么优点和缺点呢?

刚好最近也在设计一个广告请求模块,借此机会好好地理解一下建造者模式。

二、建造者模式是什么?

造者模式(Builder Pattern):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。建造者模式属于对象创建型模式。

建造者模式结构

设计模式之建造者(Builder)模式_建造者

建造者模式包含如下角色:

  • Builder:抽象建造者
  • ConcreteBuilder:具体建造者(可视情况)
  • Director:指挥者
  • Product:产品角色

建造者模式时序图

设计模式之建造者(Builder)模式_建造者_02

建造者模式的简化:

  • 省略抽象建造者角色:如果系统中只需要一个具体建造者的话,可以省略掉抽象建造者
  • 省略指挥者角色:在具体建造者只有一个的情况下,如果抽象建造者角色已经被省略掉,那么还可以省略指挥者角色,让Builder角色扮演指挥者与建造者双重角色

结合实际使用情况,下文主要介绍简化的建造者模式。

三、建造者模式解决什么问题?

在面向对象的程序设计(Object Oriented Programming, OOP)过程中,现实世界中所有的事物都可以被看作一个对象(Object)。

而无论是在现实世界中还是在软件系统中,都存在一些复杂的对象,它们拥有多个组成部分,如汽车,它包括车轮、方向盘、发送机等各种部件。而对于大多数用户而言,无须知道这些部件的装配细节,也几乎不会使用单独某个部件,而是使用一辆完整的汽车,可以通过建造者模式对其进行设计与描述,建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节。

在软件开发中,也存在大量类似汽车一样的复杂对象,它们拥有一系列成员属性,这些成员属性中有些是引用类型的成员对象。而且在这些复杂对象中,还可能存在一些限制条件,如某些属性没有赋值则复杂对象不能作为一个完整的产品使用;有些属性的赋值必须按照某个顺序,一个属性没有赋值之前,另一个属性可能无法赋值等。

复杂对象相当于一辆有待建造的汽车,而对象的属性相当于汽车的部件,建造产品的过程就相当于组合部件的过程。由于组合部件的过程很复杂,因此,这些部件的组合过程往往被“外部化”到一个称作建造者的对象里,建造者返还给客户端的是一个已经建造完毕的完整产品对象,而用户无须关心该对象所包含的属性以及它们的组装方式,这就是建造者模式的目标。

创建对象

通常,我们创建一个对象的方法是直接调用其构造方法。我们来看看一只狗狗的定义:

public class Dog {

private String color; // 毛色
private String breed; // 品种
private int age; // 年龄
private boolean isMale; // 性别

public Dog(String color, String breed, int age, boolean isMale) {
this.color = color;
this.breed = breed;
this.age = age;
this.isMale = isMale;
}

// get and set methods...
}


看起来很简单。但如果我们碰到了一个复杂的对象:

比如我们用一个class来表示车,车有一些必需的属性,比如:车身,轮胎,发动机,方向盘等。也有一些可选属性,假设超过10个,比如:车上的一些装饰,安全气囊等等非常多的属性。

使用构造器方法创建对象

如果我们用构造器来构造对象,我们的做法是 提供第一个包含4个必需属性的构造器,接下来再按可选属性依次重载不同的构造器,这样是可行的,但是会存在以下问题:

  • 一旦属性非常多,需要重载n多个构造器,而且各种构造器的组成都是在特定需求的情况下制定的,代码量多了不说,灵活性大大下降
  • 客户端调用构造器的时候,需要传的属性非常多,可能导致调用困难,我们需要去熟悉每个特定构造器所提供的属性是什么样的,而参数属性多的情况下,我们可能因为疏忽而传错顺序
public class Car {
/**
* 必需属性
*/
private String carBody; // 车身
private String tyre; // 轮胎
private String engine; // 发动机
private String aimingCircle; // 方向盘
/**
* 可选属性
*/
private String decoration; // 车内装饰品

/**
* 必需构造器
*/
public Car(String carBody, String tyre, String engine, String aimingCircle) {
this.carBody = carBody;
this.tyre = tyre;
this.engine = engine;
this.aimingCircle = aimingCircle;
}

/**
* 假如我们需要再添加车内装饰品,即在原来构造器基础上再重载一个构造器
*/
public Car(String carBody, String tyre, String engine, String aimingCircle, String decoration) {
this.carBody = carBody;
this.tyre = tyre;
this.engine = engine;
this.aimingCircle = aimingCircle;
this.decoration = decoration;
}
}


使用JavaBeans模式创建对象

简单来说就是使用set方法去设置对象属性:我们提供无参的构造函数,暴露一些公共的方法让用户自己去设置对象属性。这种方法较之第一种似乎增强了灵活度,用户可以根据自己的需要随意去设置属性。但是这种方法自身存在严重的缺点:

  1. 因为构造过程被分到了几个调用中,在构造中 JavaBean 可能处于不一致的状态。类无法仅仅通过判断构造器参数的有效性来保证一致性。
  2. 如果参数之间存在依赖关系,这种方式无法保证设置参数的顺序。
  3. 还有一个严重的弊端是,JavaBeans 模式阻止了把类做成不可变的可能,这就需要我们付出额外的操作来保证它的线程安全。
public class Car {
/**
* 必需属性
*/
private String carBody; // 车身
private String tyre; // 轮胎
private String engine; // 发动机
private String aimingCircle; // 方向盘
/**
* 可选属性
*/
private String decoration; // 车内装饰品

public void setCarBody(String carBody) {
this.carBody = carBody;
}

public void setTyre(String tyre) {
this.tyre = tyre;
}

public void setEngine(String engine) {
this.engine = engine;
}

public void setAimingCircle(String aimingCircle) {
this.aimingCircle = aimingCircle;
}

public void setDecoration(String decoration) {
this.decoration = decoration;
}
}


可见,对于复杂对象的构建,上述两种方法都存在一定的问题。建造者模式正是为解决上述问题而出现的。

四、建造者模式的特性

便捷地创建对象,用户无需关心具体创建过程

我们用户一般不会自己来完成Car组装这些繁琐的过程,而是把它交给汽车制造商。我们告诉汽车制造商我们希望的汽车是什么样子的(汽车的属性),由汽车制造商去完成汽车的组装过程,这里的Builder就是汽车制造商,我们的Car的创建都交由他来完成,我们只管开车就是啦, 先来个代码实际体验一下~

public final class Car {
/**
* 必需属性
*/
final String carBody; // 车身
final String tyre; // 轮胎
final String engine; // 发动机
final String aimingCircle; // 方向盘
final String safetyBelt; // 安全带
/**
* 可选属性
*/
final String decoration; // 车内装饰品
/**
* car 的构造器,持有 Builder, 将builder制造的组件赋值给 car 完成构建
* @param builder
*/
public Car(Builder builder) {
this.carBody = builder.carBody;
this.tyre = builder.tyre;
this.engine = builder.engine;
this.aimingCircle = builder.aimingCircle;
this.decoration = builder.decoration;
this.safetyBelt = builder.safetyBelt;
}

// get methods...

@Override
public String toString() {
return "Car{" +
"carBody='" + carBody + '\'' +
", tyre='" + tyre + '\'' +
", engine='" + engine + '\'' +
", aimingCircle='" + aimingCircle + '\'' +
", safetyBelt='" + safetyBelt + '\'' +
", decoration='" + decoration + '\'' +
'}';
}

public static final class Builder {

String carBody;
String tyre;
String engine;
String aimingCircle;
String decoration;
String safetyBelt;

public Builder() {
this.carBody = "宝马";
this.tyre = "宝马";
this.engine = "宝马";
this.aimingCircle = "宝马";
this.decoration = "宝马";
}

/**
* 实际属性配置方法
* @param carBody
* @return
*/
public Builder carBody(String carBody) {
this.carBody = carBody;
return this;
}

public Builder tyre(String tyre) {
this.tyre = tyre;
return this;
}

public Builder safetyBelt(String safetyBelt) {
if (safetyBelt == null) throw new NullPointerException("没系安全带");
this.safetyBelt = safetyBelt;
return this;
}

public Builder engine(String engine) {
this.engine = engine;
return this;
}

public Builder aimingCircle(String aimingCircle) {
this.aimingCircle = aimingCircle;
return this;
}

public Builder decoration(String decoration) {
this.decoration = decoration;
return this;
}

/**
* 最后创造出实体car
* @return
*/
public Car build() {
return new Car(this);
}
}
}


现在我们的类就写好了,我们调用的时候执行一下代码:

 Car car = new Car.Builder().build();
System.out.print(car.toString());


执行结果:

carBody = "宝马",
tyre = "宝马",
engine = "宝马",
aimingCircle = "宝马",
safetyBelt = null,
decoration = "宝马"

可以看到,我们默认的 car 已经制造出来了,默认的零件都是 "宝马",滴滴滴~来不及解释了,快上车。假如我们不使用默认值,需要自己定制的话,非常简单。只需要拿到 Builder 对象之后,依次调用指定方法,最后再调用 build 返回 car 即可。下面代码示例:

 //配置car的车身为 奔驰
Car car = new Car.Builder()
.carBody("奔驰")
.build();


执行结果:

carBody = "奔驰",
tyre = "宝马",
engine = "宝马",
aimingCircle = "宝马",
safetyBelt = null,
decoration = "宝马"

咦,神奇的定制 car 定制成功了。

对象属性一旦创建即不可变

可以看到,上述Car类中的属性均使用​​final​​关键字进行修饰,保证了Car对象一旦创建就是不可变的(属性不变)。在有些场景(如需要将Car对象作为HashMap的key时)中非常有用。

方便地对属性增加一些限制

我们在 Builder 类中的一系列构建方法中还可以加入一些我们对配置属性的限制。例如我们给 car 添加一个安全带属性,在 Buidler 对应方法出添加以下代码:

 public Builder safetyBelt(String safetyBelt) {
if (safetyBelt == null) throw new NullPointerException("没系安全带,你开个毛车啊");
this.safetyBelt = safetyBelt;
return this;
}


然后调用的时候:

 Car car = new Car.Builder()
.carBody("奔驰")
.safetyBelt(null)
.build();


我们给配置安全带属性加了 null 判断,一但配置了null 属性,即会抛出异常。

方便地对已有对象进行改造,创建新的对象

最后有客户说了,你制造出来的 car 体验不是很好,想把车再改造改造,可是车已经出厂了还能改造吗?那这应该怎么办呢?不要急,好说好说,我们只要能再拿到 Builder 对象就有办法。下面我们给 Builder 添加如下构造,再对比下 Car 的构造看看有啥奇特之处:

 /**
* Builder的构造函数:回厂重造
* @param car
*/
public Builder(Car car) {
this.carBody = car.carBody;
this.safetyBelt = car.safetyBelt;
this.decoration = car.decoration;
this.tyre = car.tyre;
this.aimingCircle = car.aimingCircle;
this.engine = car.engine;
}

/**
* Car的构造器 持有 Builder,将 builder 制造的组件赋值给 car 完成构建
*
* @param builder
*/
public Car(Builder builder) {
this.carBody = builder.carBody;
this.tyre = builder.tyre;
this.engine = builder.engine;
this.aimingCircle = builder.aimingCircle;
this.decoration = builder.decoration;
this.safetyBelt = builder.safetyBelt;
}


似乎有着对称的关系,没错。我们提供对应的构造。调用返回对应的对象,可以实现返回的效果。在 Car 中添加方法:

 /**
* 重新拿回builder 去改造car
* @return
*/
public Builder newBuilder() {
return new Builder(this);
}


现在来试试能不能返厂重建?把原来的宝马车重造成奔驰车,调用代码:

Car newCar = car.newBuilder()
.carBody("奔驰")
.safetyBelt("奔驰")
.tyre("奔驰")
.aimingCircle("奔驰")
.decoration("奔驰")
.engine("奔驰")
.build();


执行结果:

carBody = "奔驰",
tyre = "奔驰",
engine = "奔驰",
aimingCircle = "奔驰",
safetyBelt = 奔驰,
decoration = "奔驰"

已经改造好了,客户相当满意~~

至此,我们的Builder模式体验就结束了。

下面分析一下使用建造者模式时的构建过程:

  1. 新建静态内部类Builder,也就是汽车制造商,我们的Car交给他来制造,Car的属性全部复制进来
  2. 定义 Builder空构造,初始化Car默认值。这里是为了初始化构造的时候,不要再去特别定义属性,直接使用默认值
  3. 定义 Builder构造,传入Car,构造里面执行Car属性赋值给Builder对应属性的操作,目的是为了重建一个Builder进行返厂重造
  4. Builder中定义一系列方法进行属性初始化,这些方法跟JavaBeans模式构建中的方法类似,不同的是:为了方便链式调用,返回值为 Builder 类型
  5. 最后在Builder中定义​​build()​​方法返回实体Car对象,Car的构造器持有Builder,最终将Builder制造的组件赋值给Car完成构建

上文的例子是简化了的建造者模式,其中​​Car​​类对应产品角色,​​Builder​​类既是具体建造者角色,又是指挥者角色。由于我们只需要一种建造者,抽象建造者的角色被省略了。

TL;DR
  • 建造者模式将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。建造者模式属于对象创建型模式。
  • 建造者模式包含如下四个角色:
  • 抽象建造者为创建一个产品对象的各个部件指定抽象接口
  • 具体建造者实现了抽象建造者接口,实现各个部件的构造和装配方法,定义并明确它所创建的复杂对象,也可以提供一个方法返回创建好的复杂产品对象
  • 产品角色是被构建的复杂对象,包含多个组成部件
  • 指挥者负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,可以在其构造方法中调用建造者对象的部件构造与装配方法,完成复杂对象的建造
  • 在建造者模式的结构中引入了一个指挥者类,该类的作用主要有两个:一方面它隔离了客户与生产过程;另一方面它负责控制产品的生成过程。指挥者针对抽象建造者编程,客户端只需要知道具体建造者的类型,即可通过指挥者类调用建造者的相关方法,返回一个完整的产品对象。
  • 建造者模式的主要优点在于客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象,每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,符合“开闭原则”,还可以更加精细地控制产品的创建过程;其主要缺点在于由于建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,因此其使用范围受到一定的限制,如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。
  • 建造者模式适用情况包括:
  • 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性
  • 需要生成的产品对象的属性相互依赖,需要指定其生成顺序
  • 对象的创建过程独立于创建该对象的类
  • 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同类型的产品

插件介绍:不会偷懒的程序猿不是好程序猿,IDEA系列的ide有相应的插件InnerBuilder可以自动生成builder相关代码,安装自行google,使用的时候只需要在实体类中​​alt + insert​​键,会有个build按钮提供代码生成。

六、实际应用

一般如果类属性在4个以上的话,建议使用 此模式。还有如果类属性存在不确定性,可能以后还会新增属性时使用,便于扩展。

  • ​OkHttp​​源码
  • ​AlterDialog​​源码