每种设计模式的出现,都是为了解决一些编程不够优雅的问题,建造者模式也是这样。
维基百科解释是:建造者模式 Builder Pattern,又名生成器模式,是一种对象构建模式。它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。
先上一个例子
借用并改造下 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 方法修改属性值。
这个时候就该祭出今天的主角了。
网友评论