建造者模式
本文内容
1. 为什么需要建造者模式?
在开发时,我们创建一个对象常用的方式就是 new,new 一个对象是通过构造函数来完成的。那什么情况下不适合用构造函数来创建对象呢?
假设现在需要设计一个 资源池配置类 ResourcePoolConfig(类比线程池、连接池等,资源可以复用,用完后归还即可),里面有如下几个变量,有些是必填项、有些有默认值(可填可不填):
这个类的设计主要是要考虑如何处理非必填变量,一个比较简单的方法是提供一个全参构造器,规定传进来的变量如果为 null,就使用默认值。具体实现如下:
public class ResourcePoolConfig {
private static final int DEFAULT_MAX_TOTAL = 8;
private static final int DEFAULT_MAX_IDLE = 8;
private static final int DEFAULT_MIN_IDLE = 0;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
public ResourcePoolConfig(String name, Integer maxTotal, Integer maxIdle, Integer minIdle) {
if (name.isBlank()) {
throw new IllegalArgumentException("name should not be empty.");
}
this.name = name;
if (maxTotal != null) {
if (maxTotal <= 0) {
throw new IllegalArgumentException("maxTotal should be positive.");
}
this.maxTotal = maxTotal;
}
if (maxIdle != null) {
if (maxIdle < 0) {
throw new IllegalArgumentException("maxIdle should be negative.");
}
this.maxIdle = maxIdle;
}
if (minIdle != null) {
if (minIdle < 0) {
throw new IllegalArgumentException("minIdle should be negative.");
}
this.minIdle = minIdle;
}
}
// getter 略
}
这样,就设计完成了。但是如果 ResourcePoolConfig 类的 可配置参数变得更多,这样就会使得构造函数得参数列表变得很长,影响代码的可读性和易用性。在使用这个构造函数时就得非常小心的填写参数,导致非常隐蔽的 bug 出现,就像下面这样:
ResourcePoolConfig config =
new ResourcePoolConfig("dbconnectionpool", 16, null, 8, null, false , true, 10, 20,false, true);
想解决参数列表过长的问题,其实提供 setter 方法就可以解决。我们让构造函数的参数列表只包含必须填写的变量,而其他非必填的变量,给用户暴露 set 方法进行注入,可以让用户自行选择设不设置。我们参数的校验逻辑也要移动到相应的 setter 方法中去。
使用方式如下:
// ResourcePoolConfig 使用举例
ResourcePoolConfig config = new ResourcePoolConfig("dbconnectionpool");
config.setMaxTotal(16);
config.setMaxIdle(8);
一切都看起来很完美,但是,需求总是在不断变化的,如果我们还有如下要求:
- 必填项变多了,此时构造函数的参数列表还是会出现过长的情况。如果把必填项也通过 set 方法注入的话,那怎么知道这些必填项到底有没有全部赋值呢?比如用户忘记调用了几个必填项的 set 方法。
- 配置项之间存在依赖关系,如果现在规定,用户设置了非必填变量的其中一个,就必须也要设置另外两个;或者规定 maxIdle 必须要小于等于 maxTotal(本来就要考虑的)。这时这些依赖关系的检验逻辑代码应该放在哪里呢?
- 把类对象改为是不可变对象,如果我们希望 ResourcePoolConfig 类对象在创建好后,就不能在修改内部的属性值了,此时我们就不能暴露 setter 方法了。
建造者模式 就可以很好地解决上述几个要求。
2. 什么是建造者模式?
建造者模式 的定义非常抽象,也没什么用,但是还是需要了解一下,它的定义是:将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。
个人觉得可以把 “表示” 理解为不同的参数、变量之间的依赖关系等。
现在可以丢掉上面这个抽象的定义了,我们来看看如何使用建造者模式解决上面的需求。
我们可以 新定义一个 Builder 类,把校验逻辑都放到 Builder 类中,先创建建造者,并通过 setter 方法设置建造者的变量值,然后再使用 build()
方法真正创建对象,在 build()
方法中做必填项的检验、依赖关系的检验等。
接着,我们需要 把 ResourcePoolConfig 类的构造函数私有化,这样就只能通过建造者来创建类对象。而且 ResourcePoolConfig 类不要提供任何 setter 方法,这样创建出来的对象就无法被修改了(不可变对象)。
使用建造者模式后,具体实现如下:
public class ResourcePoolConfig {
private String name;
private int maxTotal;
private int maxIdle;
private int minIdle;
private ResourcePoolConfig(Builder builder) {
this.name = builder.name;
this.maxTotal = builder.maxIdle;
this.maxIdle = builder.maxIdle;
this.minIdle = builder.minIdle;
}
// getter 方法略,不提供 setter 方法
/**
* ResourcePoolConfig 的建造者,当然也可以将 Builder 类
* 设计成独立的非内部类 ResourcePoolConfigBuilder。
*/
public static class Builder {
private static final int DEFAULT_MAX_TOTAL = 8;
private static final int DEFAULT_MAX_IDLE = 8;
private static final int DEFAULT_MIN_IDLE = 0;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
/**
* 真正创建对象的方法。校验逻辑放到此方法来做,包括必填项校验、依赖关系校验等
*/
public ResourcePoolConfig build() {
if (name.isBlank()) {
throw new IllegalArgumentException("name should not be empty.");
}
if (maxIdle > maxTotal) {
throw new IllegalArgumentException("maxIdle should less equals maxTotal.");
}
if (minIdle > maxTotal || minIdle > maxIdle) {
throw new IllegalArgumentException("minIdle should less equals maxIdle and maxTotal.");
}
return new ResourcePoolConfig(this);
}
// 以下是 Builder 对外提供的 setter 方法
public Builder setName(String name) {
if (name.isBlank()) {
throw new IllegalArgumentException("name should not be empty.");
}
this.name = name;
return this;
}
public Builder setMaxTotal(int maxTotal) {
if (maxTotal <= 0) {
throw new IllegalArgumentException("maxTotal should be positive.");
}
this.maxTotal = maxTotal;
return this;
}
public Builder setMaxIdle(int maxIdle) {
if (maxIdle < 0) {
throw new IllegalArgumentException("maxIdle should be negative.");
}
this.maxIdle = maxIdle;
return this;
}
public Builder setMinIdle(int minIdle) {
if (minIdle < 0) {
throw new IllegalArgumentException("minIdle should be negative.");
}
this.minIdle = minIdle;
return this;
}
}
}
使用示例如下:
public class Usage {
public static void main(String[] args) {
// 抛出 IllegalArgumentException,因为 minIdle > maxIdle
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
.setName("dbconnectionpool")
.setMaxTotal(16)
.setMaxIdle(10)
.setMinIdle(12)
.build();
}
}
建造者模式除了能解决上述需求外,还能 避免对象存在无效状态。例如定义一个长方形类,采用先创建再 set 的方式,就会导致第一个 set 之后,对象处于无效状态。具体代码如下:
Rectangle r = new Rectange(); // r is invalid
r.setWidth(2); // r is invalid
r.setHeight(3); // r is valid
这里的无效状态会有什么影响呢?如果在并发场景下,不一次性设置好所有变量的值,那么线程就有可能获取到无效状态的对象。
为了避免这种无效状态的存在,我们就需要使用构造函数一次性初始化好所有的成员变量。如果构造函数参数过多,那么就需要使用建造者模式一次性地创建对象,从而让对象一直处于有效状态。
不过,建造者模式有一个很明显的 缺点,那就是 代码有点重复,从上面的例子可以看出,ResourcePoolConfig 类中的成员变量,要在 Builder 类中又重新再定义一遍。所以,如果你不在意上面所提到的点,那么就可以使用原始方式,直接暴露 setter 方法让外部设置变量值来创建对象即可。
3. 与工厂模式有何区别?
建造者模式与工厂模式都是用来负责创建对象的工作,它们有什么区别呢?
实际上,工厂模式是用来创建类型相关,但创建出来的对象是不同的(继承同一父类的不同子类对象),由给定的参数来决定创建哪种具体的对象。而 建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化” 地创建不同的对象(注意这里类的类型是相同的)。
一个很典型的例子就是:客户到 KFC 点餐,利用工厂模式,可以根据客户不同的选择,来制作不同的食物,比如汉堡、披萨、炸鸡(这些都可以看作是 Food 类的子类)。而对于披萨来说,客户又有各种配料可以定制,比如奶酪、西红柿、起司等,通过建造者模式,可以根据用户选择的不同配料来制作披萨(虽然都是披萨(类型相同),但制作出来的披萨是不同的(对象不同))。
4. 总结
建造者模式的原理和实现都是比较简单的,重要的是需要掌握它的应用场景。
如果类中有很多属性,避免构造函数参数列表过长,影响代码的可读性和易用性,可以先考虑使用 set 方法解决。但是,如果还有如下需求,那么就可以考虑建造者模式了:
- 必填项变多了,此时构造函数的参数列表还是会出现过长的情况。如果把必填项也通过 set 方法注入的话,那么就不知道这些必填项到底有没有全部赋值。
- 配置项之间存在依赖关系,如果现在规定,用户设置了非必填变量的其中一个,就必须也要设置另外两个;或者规定某属性必须要小于等于另一属性等。这时这些依赖关系的检验逻辑代码就没地方可放了。
- 把类对象改为是不可变对象,如果我们希望类对象在创建好后,就不能在修改内部的属性值了,此时我们就不能暴露 setter 方法了。
如果 需要保证对象一直处于有效状态,就只能使用构造函数创建对象,而不能一步一步的调用 set 方法,当使用构造函数参数列表过长时,也可以考虑使用建造者模式。
此外,还讲到了工厂模式和建造者模式的区别,具体区别如下:
- 工厂模式是用来创建类型相关,但创建出来的对象是不同的(继承同一父类的不同子类对象),由给定的参数来决定创建哪种具体的对象。
- 建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化” 地创建不同的对象(注意这里类的类型是相同的)。