之前说过了单例模式,这周想说说建造者模式,它是另外一个比较常用的创建型设计模式。
维基百科解释是:建造者模式,又名生成器模式,是一种对象构建模式。它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。
每种设计模式的出现,都是为了解决一些编程不够优雅的问题,建造者模式也是这样。
先上一个简单的例子
借用并改造下《Effective Java》中给出一个例子:每种食品包装上都会有一个营养成分表,每份的含量、每罐的含量、每份卡路里、脂肪、碳水化合物、钠等,还可能会有其他N种可选数据,大多数产品的某几个成分都有值,该如何定义营养成分这个类呢?
无参构造器
这种方式就是标准的JavaBean方式:
public class Nutrition {
private int servingSize = -1;// required
private int servings = -1;// required
private int calories;// optional
private int fat;// optional
private int sodium;// optional
private int carbohydrate;// optional
public Nutrition() {
}
// getter and setter
}
营养成分列表实例构造过程中,需要多次调用set方法,也就是将构造实例的过程拆分成多步。这种方式有一个明显的缺点,没有办法校验必要的参数和关联参数的有效性,比如servingSize是必要参数,如果没有set值,只有在使用的时候才能校验。再比如calories和fat有关联性,两个属性必须同时有值或同时为0,就需要额外在定义校验方法。
这两点限制都只能在客户端进行校验:
- 隐式校验,在
Nutrition
类中定义校验方法,由客户端调用校验方法 - 显示校验,由客户端校验对象属性的合法性。
无论哪种校验方法,都需要客户端参与才行,如果客户端忘记了,那数据有效性就没有办法保证了。
重叠构造器
这种方式也是比较常用的一种方式,既然无参构造器不能满足需求,那就定义多个有参构造器:第一个构造器只有必传参数,第二个构造器在第一个基础上加一个可选参数,第三个加两个,以此类推,直到最后一个包含所有参数。
代码如下:
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,参数不多的情况下,也能接受。但是如果参数比较多,假设20个参数,可选参数中只有最后一个值不是0,写起来是不是就恶心了。
还有一个隐藏缺点,那就是如果同类型参数比较多,比如上面这个例子,都是int
类型,除非每次创建实例的时候仔细对比方法签名,否则很容易传错参数,这种错误编辑器检查不出来,只有在运行时才能发现各种诡异错误。
这种方式解决了第一种方式的缺点,但是也引入了新的问题,所以需要一种更好的替代方案。
建造者模式
直接上代码
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类,相当省事。
再来一个例子
上面的例子已经覆盖大多数情况了,下面再来一种情况。例子来源自《大话设计模式》:现在需要画个小人,需要头、身体、左手、右手、左脚、右脚,但是需要画出高矮胖瘦各种不同的小人,。
通过建造者模式也可以实现这个需求,代码如下:
public class PersonBuilder {
@Override
public void buildHead() {
System.out.println("头");
}
@Override
public void buildBody() {
System.out.println("身体");
}
@Override
public void buildLeftHand() {
System.out.println("左手");
}
@Override
public void buildRightHand() {
System.out.println("右手");
}
@Override
public void buildLeftLeg() {
System.out.println("左腿");
}
@Override
public void buildRightLeg() {
System.out.println("右腿");
}
}
但是,如果有个方法忘记调用了,比如画右手的方法忘记调用了,那就成杨过大侠了。这个时候就需要在建造者类之上加一个Director
类,俗称监工。
public class PersonDirector {
private final PersonBuilder pb;
public PersonDirector(final PersonBuilder pb) {
this.pb = pb;
}
public void createPerson() {
this.pb.buildHead();
this.pb.buildBody();
this.pb.buildLeftHand();
this.pb.buildRightHand();
this.pb.buildLeftLeg();
this.pb.buildRightLeg();
}
}
这个时候,对于客户端来说,只需要关注Director
类就行了,就相当于在客户端调用构造器之间,增加一个监工,一个对接人,保证客户端能够正确使用Builder
类。
细心的朋友可能会发现,我这里的Director
类的构造函数增加了一个Builder
参数,这是为了更好的扩展,比如,这个时候需要增加一个胖子Builder
类,那就只需要定义一个FatPersonBuilder
,继承PersonBuilder
,然后只需要将新增加的类传入Director
的构造函数即可。
这也是建造者模式的另一个优点:可以定义不同的Builder
类实现不同的构建属性,比如上面的普通人和胖子两个Builder
类。
最后来个总结
上面给出了建造者模式两种使用方法,第二种方法中的Director
类相当于在客户端之间增加一个监工,第一种方法是把监工的工作放在客户端完成,两种方法只是在表现形式上的存在差异。
下面来总结下建造者模式的优点:
- 将一个复杂对象的创建过程封装起来,向客户端隐藏产品内部表现
- 允许对象通过多个步骤来创建,并可以改变过程
- 产品的实现可以变换,因为客户端只能看到一个抽象的接口
建造者模式作为一种比较实用的设计模式,应用场景主要是下面两个:
- 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时
- 当构造过程必须允许被构造的对象有不同的表示时
碰到上面两种情况,不要犹豫,果断使用建造者模式就行。
个人主页: https://www.howardliu.cn
个人博文: 设计模式:建造者模式
CSDN主页: http://blog.csdn.net/liuxinghao
CSDN博文: 设计模式:建造者模式