前言
我们都知道设计模式分为创建型,结构型和行为型。创建型有,单例模式,工厂模式,建造者模式和原型模式。
今天,我们再来学习另外一个比较常用的创建型设计模式,Builder 模式,中文翻译为建造者模式或者构建者模式,也有人叫它生成器模式。
很多博客总结的关于建造者模式的作用是:创建复杂对象的时候,用建造者模式可以使客户端不必知道产品内部组成的细节。 这虽然是一个很重要的特征,但是还有一个特征以及作用很多人都并不知道。我们接下来就来好好的探讨一番。
建造者模式的作用
在平时的开发中,创建一个对象最常用的方式是,使用 new 关键字调用类的构造函数来完成。虽然这是最简单最常用的,但是我们仔细想一想真的是所有场景都适用吗?
场景分析
有如下的一个场景:
有一个工厂类,这个工厂类有如下几个成员变量:
1. 工厂名字
2. 工厂员工列表
3. 工厂设备列表
要想让这个工厂正常的运行,这3个成员变量必须被正确赋值。那么此时,我们最常见的方法就是在构造方法中实现这些成员变量的赋值。
当成员变量不多的时候,像上诉说的3个,这样并没有什么问题,但是当成员变量变成6个,12个甚至更多,那继续沿用现在的设计思路,构造函数的参数列表会变得很长,代码在可读性和易用性上都会变差。在使用构造函数的时候,我们就容易搞错各参数的顺序,传递进错误的参数值,导致非常隐蔽的 bug。
解决办法
解决这个问题的办法你应该也已经想到了,那就是用 set() 函数来给成员变量赋值,以替代冗长的构造函数。我们将必填的成员变量,放在我们的构造方法中,强制创建类对象的时候就要填写。将其他不是必填的成员变量我们通过 set() 函数来设置,让使用者自主选择填写或者不填写。
这样代码在可读性和易用性上提高了很多。
引入构造模式
当我们的如下几个需求的时候,上诉使用set()函数的设计思路可能就不太满足了
- 我们刚刚说的,将必填的成员变量放到构造方法。如果必填项很多,那构造方法又会出现我们之前说的那个问题。但是,如果我们把必填项也通过 set() 方法设置,那校验这些必填项是否已经填写的逻辑就无处安放了。
- 除此之外,假设配置项之间有一定的依赖关系,比如,如果用户设置了员工列表, 就必须显式地设置员工工资;或者配置项之间有一定的约束条件,比如,员工和工资必须一一对应。如果我们继续使用现在的设计思路,那这些配置项之间的依赖关系或者约束条件的校验逻辑就无处安放了。
- 如果我们希望这个工厂类对象某些属性如name是不可变的,也就是说,对象在创建好之后,就不能再修改这些内部的属性值。要实现这个功能,我们就不能在外部暴露 set() 方法。
为了解决这些问题,建造者模式就派上用场了。
使用建造者来实现我们的需求
我们可以把校验逻辑放置到 Builder 类中,先创建建造者,并且通过 set() 方法设置建造者的变量值,然后在使用 build() 方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象。除此之外,我们把 Factory 的构造函数改为 private 私有权限。这样我们就只能通过建造者来创建 Factory 类对象。并且,Factory 没有为不可变属性提供任何 set() 方法,这样我们创建出来的对象就做到了相对不可。代码如下:
public class Factory {
private String factoryName; //工厂名字
private List employeeIds; //员工列表
private Map salaryMap; //员工工资
private List equipmentName; //设备列表
//私有构造方法
private Factory(){}
private Factory(Builder builder) {
this.factoryName = builder.factoryName;
this.employeeIds = builder.employeeIds;
this.salaryMap = builder.salaryMap;
this.equipmentName = builder.equipmentName;
}
//...省略getter方法...
//我们将Builder类设计成了Factory的内部类。
//我们也可以将Builder类设计成独立的非内部类FactoryBuilder。
public static class Builder {
private String factoryName;
private List employeeIds;
private Map salaryMap;
private List equipmentName;
public Factory build() {
// 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
if (StringUtils.isBlank(factoryName)) {
throw new IllegalArgumentException("...");
}
for(Integer employeeId : employeeIds){
if (salaryMap.get(employeeId) == null) {
throw new IllegalArgumentException("...");
}
}
return new ResourcePoolConfig(this);
}
public Builder setFactoryName(String factoryName) {
if (StringUtils.isBlank(factoryName)) {
throw new IllegalArgumentException("...");
}
this.factoryName = factoryName;
return this;
}
public Builder setEmployeeIds(List employeeIds) {
// 员工不能为0
if (employeeIds.size() == 0) {
throw new IllegalArgumentException("...");
}
this.employeeIds = employeeIds;
return this;
}
public Builder setSalaryMap(Map salaryMap) {
if (salaryMap.size() == 0) {
throw new IllegalArgumentException("...");
}
this.salaryMap = salaryMap;
return this;
}
public Builder setEquipmentName(List equipmentName) {
// 设备必须大于两个
if (equipmentName.siez() < 2) {
throw new IllegalArgumentException("...");
}
this.minIdle = minIdle;
return this;
}
}
}
// 这段代码会抛出IllegalArgumentException,因为设备只有一台
List employeeIds = Collections.singletonList(1);
Map salaryMap = Collections.singletonMap(1, 9000);
List equipmentName = Collections.singletonList("新型设备");
Factory factory = new Factory.Builder()
.setFactoryName("万能工厂")
.setEmployeeIds(employeeIds)
.setSalaryMap(salaryMap)
.setEquipmentName(equipmentName)
.build();
总结
我们来与工厂模式做一个对比,建造者模式是让建造者类来负责对象的创建工作。工厂模式,是由工厂类来负责对象创建的工作。
那它们之间有什么区别呢?实际上,工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。
建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。网上有一个经典的例子很好地解释了两者的区别。
顾客走进一家餐馆点餐,我们利用工厂模式,根据用户不同的选择,来制作不同的食物,比如披萨、汉堡、沙拉。对于披萨来说,用户又有各种配料可以定制,比如奶酪、西红柿、起司,我们通过建造者模式根据用户选择的不同配料来制作披萨。
实际上,我们也不要太学院派,非得把工厂模式、建造者模式分得那么清楚,我们需要知道的是,每个模式为什么这么设计,能解决什么问题。只有了解了这些最本质的东西,我们才能不生搬硬套,才能灵活应用,甚至可以混用各种模式创造出新的模式,来解决特定场景的问题。