在 《effective JAVA》中有这么一条,遇到多个构造器参数时要考虑用构建器(Builder)。那么为什么要用Builder呢。
首先我们来看看我们传统创建不同参数对象的方法。
public class BuilderTest {
private int servingSize;
private int servings;
private int calories;
private int fat;
public BuilderTest(int servingsSize,int servings) {
this.servingSize = servingsSize;
this.servings = servings;
}
public BuilderTest(int servingsSize,int servings,int calories) {
this.servingSize = servingsSize;
this.servings = servings;
this.calories = calories;
}
public BuilderTest(int servingsSize,int servings,int calories,int fat) {
this.servingSize = servingsSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
}
}
当你想要创建实例的时候,就利用参数列表最短的构造器,但当参数多的时候,它就会很快失去控制,当你看着这么多的构造函数可能就懵逼了。一句话重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写,并且仍然比较难以阅读。这时候我们可能会想到另外的一种执行方法。采用JavaBean模式。在这种模式下,调用一个无参构造器来创建对象。(这也就是为什么在spring中配置Bean的时候必须要有一个共有的无参构造器的原因)。
public class BuilderTest {
private int servingSize = -1;
private int servings = -1;
private int calories = 0;
private int fat = 0;
public BuilderTest(){}
public void setServingsSize(int val){
this.servingSize = val;
}
public void setServings(int val){
this.servings = val;
}
public void setCalories(int val){
this.calories = val;
}
public void setFat(int val){
this.fat = val;
}
public static void main(String[] args) {
BuilderTest builderTest = new BuilderTest();
builderTest.setServingsSize(10);
builderTest.setServings(1);
}
}
这样我们就可以通过创建一个无参的对象,通过这个对象进行设置它的参数,而且可读性也比较强。但是JavaBean模式本身有一个很严重的缺点。因为构造过程被分配到了几个调用中,在构造过程中JavaBean可能处于不一致的状态。(这是什么意思呢?我的理解是我们通过一个无参的构造函数创建一个对象只能保证这个对象是线程安全的,但是当通过这个对象去调用方法设置属性的时候就无法保证多个线程竞争对象,从而导致线程不安全)类无法仅仅通过检验构造器参数的有效性来保证一致性。
所以上面这两种模式都存在弊端,这时候就需要建造者模式来解决这个问题了。它既能保证像重叠构造器模式那样的安全性,也能保证像JavaBean模式那么好的可读性。
它的过程是不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个builder对象,然后客户端在builder对象上调用类似于setter的方法, 来设置每个相关的可选参数。最后客户端用无参的builder方法来生成不可变的对象。
public class BuilderModel {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
//建造者
public static class Builder{
private final int servingsSize;
private final int servings;
private int calories = 0;
private int fat = 0;
public Builder(int servingsSize,int servings){
this.servingsSize = servingsSize;
this.servings = servings;
}
//创建各个组件
public Builder calories(int val){
calories = val;
return this;
}
public Builder fat(int val){
fat = val;
return this;
}
//返回这个模型
public BuilderModel builder(){
return new BuilderModel(this);
}
}
private BuilderModel(Builder builder){
servingSize = builder.servingsSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
}
@Override
public String toString() {
return "BuilderModel [servingSize=" + servingSize + ", servings=" + servings + ", calories=" + calories
+ ", fat=" + fat + "]";
}
public static void main(String[] args) {
BuilderModel builder = new BuilderModel.Builder(230, 0).calories(10).fat(35).builder();
System.out.println(builder);
}
}
如果上面这个例子还不够具体,我们再来看一个生活中的例子,参考至这个文章
实例概括:
背景:小成希望去电脑城买一台组装的台式主机
过程:
(1)电脑城老板(Diretor)和小成(Client)进行需求沟通(买来打游戏?学习?看片?)
(2)了解需求后,电脑城老板将小成需要的主机划分为各个部件(Builder)的建造请求(CPU、主板blabla)
(3)指挥装机人员(ConcreteBuilder)去构建组件;
(4)将组件组装起来成小成需要的电脑(Product)
1、定义组装的过程,也就是上面的builder,不过将这里将其实现交给子类
public abstract class Builder {
//第一步:装cpu,声明为抽象的,由子类来完成,也就是各个部分的工人完成
public abstract void BuilderCPU();
//装主板,一样
public abstract void BuilderMainboard();
//装硬盘
public abstract void BuilderHD();
//第二步,返回产品,组装好的电脑
public abstract Computer GetComputer();
}
2、具体的组装过程
//Builder的具体实现类
public class ConcreteBuilder extends Builder {
Computer computer = new Computer();
@Override
public void BuilderCPU() {
computer.add("组装CPU");
}
@Override
public void BuilderMainboard() {
computer.add("组装主板");
}
@Override
public void BuilderHD() {
computer.add("组装HD");
}
@Override
public Computer GetComputer() {
return computer;
}
}
上面的1,2步骤都是对应于第一个例子的Builder
3、定义老板(指挥人员),跟客户沟通的那个人,这个人告诉组装人员要哪些配件
public class Director {
//指挥装机人员组装电脑
public void Construct(Builder builder){
builder.BuilderCPU();
builder.BuilderMainboard();
builder.BuilderHD();
}
}
而这一步相当于上面的BuilderModel的过程,告诉组装人员,我需要哪些配置
4、定义一个电脑所需要的东西,也就是我们的具体对象
import java.util.ArrayList;
import java.util.List;
public class Computer {
//一台电脑的组件
private List<String> parts = new ArrayList<>();
//组装各个部件
public void add(String part){
parts.add(part);
}
//交付电脑
public void show(){
for (String string : parts) {
System.out.println("组件"+string+"组装完成");
}
System.out.println("电脑组装完成,请验收");
}
}
而这就是最后的创建对象过程。
客户端调用过程,也就是客户买电脑
public class BuilderPattern {
public static void main(String[] args) {
//找到老板
Director director = new Director();
//装机人员
Builder builder = new ConcreteBuilder();
//老板让装机人员去装电脑
director.Construct(builder);
//装好电脑,把电脑拿过来
Computer computer = builder.GetComputer();
//电脑给客户
computer.show();
}
}
输出:
这时当我们不需要HD这个配件的时候,老板就可以直接不告诉组装人员即可
public class Director {
//指挥装机人员组装电脑
public void Construct(Builder builder){
builder.BuilderCPU();
builder.BuilderMainboard();
// builder.BuilderHD();
}
}
我们把通知组装HD的给注释掉
输出:
这样就可以达到一种高度的灵活性
Builder具有下面的优缺点
优点
①易于解耦
将产品本身与产品创建过程进行解耦,可以使用相同的创建过程来得到不同的产品。也就说细节依赖抽象。
易于精确控制对象的创建
将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰
②易于拓展
增加新的具体建造者无需修改原有类库的代码,易于拓展,符合“开闭原则“。
每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。
缺点
①建造者模式所创建的产品一般具有较多的共同点,其组成部分相似;如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
②如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。