设计模式系列 | 建造者模式
想自己的开发路子走得更远更久,想成为更牛的码农,那设计模式的理解和掌握是必须的。
很多人也都听说过建造者设计模式,但总是对这个设计模式理解得不够透彻,今天我们就来聊聊建造者设计模式。另外也说说建造者设计模式和工厂模式的区别。
定义
其实建造者设计模式的定义,很多事看不懂的,也是记不住的,但我们还是得先来看看是如何定义的。
❝
The intent of the Builder design pattern is to separate the construction of a complex object from its representation. By doing so the same construction process can create different representations.
❞
将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示
另外在维基百科解释是:
❝
建造者模式 Builder Pattern,又名生成器模式,是一种对象构建模式。它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。
❞
是不是觉得非常的不好理解?
下面我们就用生活中的案例,反过来理解建造者设计模式的定义会更好。
案例1
借用并改造下 Effective Java 中给出的例子:每种食品包装上都会有一个营养成分表,每份的含量、每罐的含量、每份卡路里、脂肪、碳水化合物、钠等,还可能会有其他 N 种可选数据,大多数产品的某几个成分都有值,该如何定义营养成分这个类呢?
重叠构造器
因为有多个参数,有必填、有选填,最先想到的就是定义多个有参构造器:第一个构造器只有必传参数,第二个构造器在第一个基础上加一个可选参数,第三个加两个,以此类推,直到最后一个包含所有参数,这种写法称为重叠构造器,有点像叠罗汉。还有一种常见写法是只写一个构造函数,包含所有参数。
代码如下:
public class Nutrition {
private int servingSize;// required
private int servings;// required
private int calories;// optional
private int fat;// optional
private int sodium;// optional
private int carbohydrate;// optional
public Nutrition(final int servingSize, final int servings) {
this(servingSize, servings, 0, 0, 0, 0);
}
public Nutrition(final int servingSize, final int servings, final int calories) {
this(servingSize, servings, calories, 0, 0, 0);
}
public Nutrition(final int servingSize, final int servings, final int calories, final int fat) {
this(servingSize, servings, calories, fat, 0, 0);
}
public Nutrition(final int servingSize, final int servings, final int calories, final int fat, final int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public Nutrition(final int servingSize, final int servings, final int calories, final int fat, final int sodium, final int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
// getter
}
这种写法还可以有效解决参数校验,只要在构造器中加入参数校验就可以了。
如果想要初始化实例,只需要 new 一下就行:
new Nutrition(100, 50, 0, 35, 0, 10)
这种写法,不够优雅的地方是,当 calories 和 sodium 值为 0 的时候,也需要在构造函数中明确定义是 0,示例中才 6 个参数,也能勉强接受。但是如果参数达到 20 个呢?可选参数中只有一个值不是 0 或空,写起来很好玩了,满屏全是 0 和 null 的混合体。
还有一个隐藏缺点,那就是如果同类型参数比较多,比如上面这个例子,都是 int 类型,除非每次创建实例的时候仔细对比方法签名,否则很容易传错参数,而且这种错误编辑器检查不出来,只有在运行时会出现各种诡异错误,排错的时候不知道要薅掉多少根头发了。
想要解决上面两个问题,不难想到,可以通过 set 方法一个个赋值就行了。
set 方式赋值
既然构造函数中放太多参数不够优雅,还有缺点,那就换种写法,构造函数只保留必要字段,其他参数的赋值都用 setter 方法就行了。
代码如下:
public class Nutrition {
private final int servingSize;// required
private final int servings;// required
private int calories;// optional
private int fat;// optional
private int sodium;// optional
private int carbohydrate;// optional
public Nutrition(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
// getter and setter
}
这样就可以解决构造函数参数太多、容易传错参数的问题,只在需要的时候 set 指定参数就行了。
如果没有特殊需求,到这里可以解决大部分问题了。
但是需求总是多变的,总会有类似“五彩斑斓的黑”这种奇葩要求:
如果必填参数比较多,或者大部分参数是必填参数。这个时候这种方式又会出现重叠构造器那些缺点。 如果把所有参数都用 set 方法赋值,那又没有办法进行必填项的校验。 如果非必填参数之间有关联关系,比如上面例子中,脂肪 fat 和碳水化合物 carbohydrate 有值的话,卡路里 calories 一定不会为 0。但是使用现在这种设计思路,属性之间的依赖关系或者约束条件的校验逻辑就没有地方定义了。 如果想要把 Nutrition 定义成不可变对象的话,就不能使用 set 方法修改属性值。
这个时候就该祭出今天的主角了。
建造者模式
先上代码
public class Nutrition {
private int servingSize;// required
private int servings;// required
private int calories;// optional
private int fat;// optional
private int sodium;// optional
private int carbohydrate;// optional
public static class Builder {
private final int servingSize;// required
private final int servings;// required
private int calories;// optional
private int fat;// optional
private int sodium;// optional
private int carbohydrate;// optional
public Builder(final int servingSize, final int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder setCalories(final int calories) {
this.calories = calories;
return this;
}
public Builder setFat(final int fat) {
this.fat = fat;
return this;
}
public Builder setSodium(final int sodium) {
this.sodium = sodium;
return this;
}
public Builder setCarbohydrate(final int carbohydrate) {
this.carbohydrate = carbohydrate;
return this;
}
public Nutrition build() {
// 这里定义依赖关系或者约束条件的校验逻辑
return new Nutrition(this);
}
}
private Nutrition(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
// getter
}
想要创建对象,只要调用 new Nutrition.Builder(100, 50).setFat(35).setCarbohydrate(10).build() 就可以了。这种方式兼具前两种方式的优点:
能够毫无歧义且明确 set 指定属性的值; 在 build 方法或 Nutrition 构造函数中定义校验方法,可以在创建对象过程中完成校验。
建造者模式的缺点就是代码变多了(好像所有的设计模式都有这个问题),这个缺点可以借助 Lombok 来解决,通过注解@Builder,可以在编译过程自动生成对象的 Builder 类,相当省事。
案例2
接下来分析下《大话设计模式》中的一个例子,这个例子从代码结构上,和建造者模式有很大的出入,但是作者却把它归为建造者模式。下面我们就来看看究竟:现在需要画个小人,一个小人需要头、身体、左手、右手、左脚、右脚。
代码如下:
public class Person {
private String head;
private String body;
private String leftHand;
private String rightHand;
private String leftLeg;
private String rightLeg;
// getter/setter
}
public class PersonBuilder {
private Person person = new Person();
public PersonBuilder buildHead() {
person.setHead("头");
return this;
}
public PersonBuilder buildBody() {
person.setBody("身体");
return this;
}
public PersonBuilder buildLeftHand() {
person.setLeftHand("左手");
return this;
}
public PersonBuilder buildRightHand() {
person.setRightHand("右手");
return this;
}
public PersonBuilder buildLeftLeg() {
person.setLeftLeg("左腿");
return this;
}
public PersonBuilder buildRightLeg() {
person.setRightLeg("右腿");
return this;
}
public Person getResult() {
return this.person;
}
}
但是,如果有个方法忘记调用了,比如画右手的方法忘记调用了,那就成杨过大侠了。这个时候就需要在 PersonBuilder 之上加一个 Director 类,俗称监工。
public class PersonDirector {
private final PersonBuilder pb;
public PersonDirector(final PersonBuilder pb) {
this.pb = pb;
}
public Person createPerson() {
this.pb
.buildHead()
.buildBody()
.buildLeftHand()
.buildRightHand()
.buildLeftLeg()
.buildRightLeg();
return this.pb.getResult();
}
}
这个时候,对于客户端来说,只需要关注 Director 类就行了,就相当于在客户端调用构造器之间,增加一个监工、一个对接人,保证客户端能够正确使用 Builder 类。
细心的朋友可能会发现,我这里的 Director 类的构造函数增加了一个 Builder 参数,这是为了更好的扩展。
比如,这个时候需要增加一个胖子 Builder 类,那就只需要定义一个 FatPersonBuilder,继承 PersonBuilder,然后只需要将新增加的类传入 Director 的构造函数即可。
这也是建造者模式的另一个优点:可以定义不同的 Builder 类实现不同的构建属性,比如上面的普通人和胖子两个 Builder 类。
框架中的应用
建造者设计模式,在JDK、Mybatis、Spring等框架源码中,得到了大量的应用。
在JDK源码中的应用
JDK 的 StringBuilder 类中提供了 append() 方法,这就是一种链式创建对象的方法,开放构造步骤,最后调用 toString() 方法就可以获得一个完整的对象。StringBuilder 类源码如下:
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence {
...
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
...
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
...
}
另外在JDK中还有以下这些也用到了建造者设计模式:
· java.lang.StringBuffer#append()
· java.nio.ByteBuffer#put() (CharBuffer, ShortBuffer, IntBuffer,LongBuffer, FloatBuffer 和DoubleBuffer与之类似)
· javax.swing.GroupLayout.Group#addComponent()
· java.sql.PreparedStatement
· java.lang.Appendable的所有实现类
在Mybatis中的应用
MyBatis 中 SqlSessionFactoryBuiler 类用到了建造者模式。且在 MyBatis 中 SqlSessionFactory是由 SqlSessionFactoryBuilder 产生的,代码如下:
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
DefaultSqlSessionFactory 的构造器需要传入 MyBatis 核心配置类 Configuration 的对象作为参数,而 Configuration 庞大复杂,初始化比较麻烦,因此使用了专门的建造者 XMLConfigBuilder 进行构建。
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 创建建造者XMLConfigBuilder实例
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// XMLConfigBuilder的parse()构建Configuration实例
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
XMLConfigBuilder 负责 Configuration 各个组件的创建和装配,整个装配的流程化过程如下:
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// Configuration#
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
XMLConfigBuilder 负责创建复杂对象 Configuration,其实就是一个具体建造者角色。SqlSessionFactoryBuilder 只不过是做了一层封装去构建 SqlSessionFactory 实例,这就是建造者模式简化构建的过程。
在Spring中的应用
比如UriComponentsBuilder 类中:
这里就不详细说应用的目的和实现的功能。因为这里还能扯很久,我们只是要知道建造者设计模式的使用也是非常广泛的,由此可知,此设计模式还是相当重要的。
总结
建造者模式的类图
下面是从网上找了一张建造者设计模式的类图:
建造者模式优缺点
建造者模式的优点有:
1、封装性好,创建和使用分离 2、扩展性好,建造类之间独立,一定程度上实现了解耦
建造者模式的缺点有:
1、产生多余的Builder对象 2、产品内部发生变化时,建造者都需要修改,成本较大
角色及其职责
Director:指挥者/导演类,负责安排已有模块的顺序,然后告诉Builder开始建造。 Builder:抽象建造者,规范产品的组建,一般由子类实现。 ConcreteBuilder:具体建造者,实现抽象类定义的所有方法,并且返回一个组建好的对象。 Product:产品类,通常实现了模板方法模式。
建造者模式和工厂模式区别
建造者模式优点类似于工厂模式,都是用来创建一个对象,但是他们还是有很大的区别,主要区别如下:
1、建造者模式更加注重方法的调用顺序,工厂模式注重于创建完整对象 2、建造者模式根据不同的产品零件和顺序可以创造出不同的产品,而工厂模式创建出来的产品都是一样的 3、建造者模式使用者需要知道这个产品有哪些零件组成,而工厂模式的使用者不需要知道,直接创建就行
彩蛋
偷偷的告诉你一个小技巧,一旦看到某某类是以Builder结尾的命名,咱们第一印象应该是猜想这里是不是用到了建造者设计模式呢?
推荐阅读
点赞越多,bug越少