1、什么是建造者模式?
Builder 模式,中文翻译为建造者模式或者构建者模式,也有人叫它生成器模式。
它可以将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。
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.
2、为什么要使用建造者模式?
2.1、关于流程化
鱼香肉丝几乎是所有大小中餐饭店都有的一道菜,但却可以吃出上万种口味来,这是为什么?
因为厨师不一样、盐多盐少,炒的火候时间的长短,都是不一样的。
为什么麦当劳、肯德基这些不过百年的洋快餐能在有千年饮食文化的中国发展得这么好?
因为他们比较规范,味道还行,而且基本不管在哪家店里吃,什么时间去吃,至少在中国,味道几乎都是一样的。由于他们制定了规范的工作流程,原料放多少,加热几分钟,都有严格规定,估计放多少盐都是用克来计量的。而这个工作流程是在所有的门店都必须要遵照执行的,所以我们吃到的东西不管在哪在什么时候味道都一样。
如果老肯发现鸡翅烤得有些焦,他们会调整具体的工作流程中的烧烤时间,如果新加一种汉堡,做法都相同,只是配料不相同,工作流程是不变的,只是加了一种具体产品而已。
抽象不应该依赖细节,细节应该依赖于抽象 ,由于我们要吃的菜都依赖于厨师这样的细节,所以我们
就很被动。
虽然工作流程也是细节,但我们去快餐店消费,我们不用关心他们的工作流程,我们更关心的是是否好吃。
这里工作流程可以是一种抽象的流程,具体放什么配料、烤多长时间等细节依赖于这个抽象。
讲到这里,我相信你已经对流程化有了一定的认识。
2.2、创建复杂对象
在平时的开发中,创建一个对象最常用的方式是,使用 new 关键字调用类的构造函数来完成。当创建一个对象的的流程比较繁琐,且有一系列的限制条件时,就要考虑是否使用建造者模式了。
如果一个类中有很多属性,为了避免构造函数的参数列表过长,影响代码的可读性和易用性,我们可以通过构造函数配合 set() 方法来解决。但是,如果存在下面情况中的任意一种,我们就要考虑使用建造者模式了。
1)必填的属性比较多
我们把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填的属性有很多,把这些必填属性都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果我们把必填属性通过 set() 方法设置,那校验这些必填属性是否已经填写的逻辑就无处安放了。
2)属性之间有一定的依赖或约束关系
如果类的属性之间有一定的依赖关系或者约束条件,我们继续使用构造函数配合 set() 方法的设计思路,那这些依赖关系或约束条件的校验逻辑就无处安放了。
3)该复杂对象不可变
如果我们希望创建不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,我们就不能在类中暴露 set() 方法。构造函数配合 set() 方法来设置属性值的方式就不适用了。
3、例子
下面我会举几个例子,为了方便你理解,难易度会逐渐增加,不过例子会逐渐贴近真实的开发场景。
3.1、制造品牌电脑(简单)
这是一个比较符合建造者模式的例子,在实际使用中,你可以对其进行改造,以符合你的使用习惯和业务需求。
以制造一台联想电脑为例,业界都有一套标准的电脑制造流程,但联想希望修改这套标准化流程,以适应自家的需求。
1、定义 Computer 电脑类,描述电脑的属性和功能
2、定义 ComputerBuilder 电脑制造者抽象类,描述装配电脑具体有哪些通用的流程
3、定义 LenovoComputerBuilder 联想电脑制造者类,它实现了装配电脑的所有通用流程
4、定义 ComputerBuilderDirector 电脑制造指挥者:由指挥者指定具体的制造流程,并让建造者按照标准流程制造。
public class Main {public static void main(String[] args) {ComputerBuilderDirector director = new ComputerBuilderDirector();ComputerBuilder builder = new LenovoComputerBuilder("联想","英特尔i7","三星 16GB","三星EVO 970 1TB").setDisplay("LG 4K 显示器").setMouse("罗技 G603 鼠标").setKeyboard("Cherry G3885 机械键盘");// 以流程1来制造Computer computer = director.CreateComputer(builder);// 以流程2来制造// Computer computer = director.CreateComputer2(builder);computer.showInfo();computer.run();}}/*** 1、Computer 电脑类*/class Computer {protected String band;//必须protected String cpu;//必须protected String ram;//必须protected String disk;//必须protected String keyboard;//可选protected String mouse;//可选protected String display;//可选public String getBand() {return band;}public void setBand(String band) {this.band = band;}public String getCpu() {return cpu;}public void setCpu(String cpu) {this.cpu = cpu;}public String getRam() {return ram;}public void setRam(String ram) {this.ram = ram;}public String getDisk() {return disk;}public void setDisk(String disk) {this.disk = disk;}public String getKeyboard() {return keyboard;}public void setKeyboard(String keyboard) {this.keyboard = keyboard;}public String getMouse() {return mouse;}public void setMouse(String mouse) {this.mouse = mouse;}public String getDisplay() {return display;}public void setDisplay(String display) {this.display = display;}@Overridepublic String toString() {return "Computer{" +"band='" + band + '\'' +", cpu='" + cpu + '\'' +", ram='" + ram + '\'' +", disk='" + disk + '\'' +", keyboard='" + keyboard + '\'' +", mouse='" + mouse + '\'' +", display='" + display + '\'' +'}';}public void run() {System.out.println("电脑启动中");}public void showInfo() {System.out.println("本电脑详细信息:" + this.toString());}}/*** 2、电脑制造者:规定了装配电脑具体有哪些流程*/abstract class ComputerBuilder {public abstract void setBand();public abstract void addCpu();public abstract void addRam();public abstract void addDisk();public abstract void addKeyboard();public abstract void addMouse();public abstract void addDsiplay();public abstract void addSomethingElse();public abstract Computer building();}/*** 3、联想电脑制造者:实现了装配电脑的所有具体流程*/class LenovoComputerBuilder extends ComputerBuilder {protected Computer computer;protected String band;//必须protected String cpu;//必须protected String ram;//必须protected String disk;//必须protected String keyboard;//可选protected String mouse;//可选protected String display;//可选// 必选项由 具体建造者的构造方法来指定public LenovoComputerBuilder(String band, String cpu, String ram, String disk) {computer = new Computer();this.band = band;this.cpu = cpu;this.ram = ram;this.disk = disk;}// 三个可选的部件public LenovoComputerBuilder setKeyboard(String keyboard) {this.keyboard = keyboard;return this;}public LenovoComputerBuilder setMouse(String mouse) {this.mouse = mouse;return this;}public LenovoComputerBuilder setDisplay(String display) {this.display = display;return this;}@Overridepublic void setBand() {computer.setBand(this.band);System.out.println("给OEM电脑贴 " + this.band + "的牌");}@Overridepublic void addCpu() {computer.setCpu(this.cpu);System.out.println("给联想电脑装配 " + this.cpu + " 处理器");}@Overridepublic void addRam() {computer.setRam(this.ram);System.out.println("给联想电脑装配" + this.ram + "内存条");}@Overridepublic void addDisk() {computer.setDisk(this.disk);System.out.println("给联想电脑装配" + this.disk + "硬盘");}@Overridepublic void addKeyboard() {computer.setKeyboard(this.keyboard);System.out.println("给联想电脑装配" + this.keyboard + "键盘");}@Overridepublic void addMouse() {computer.setMouse(this.mouse);System.out.println("给联想电脑装配" + this.mouse + "鼠标");}@Overridepublic void addDsiplay() {computer.setDisplay(this.display);System.out.println("给联想电脑装配" + this.display + "显示器");}@Overridepublic void addSomethingElse() {System.out.println("给联想电脑做最后的出厂检验");}@Overridepublic Computer building() {return this.computer;}}/*** 4、指挥者:由指挥者指定具体的制造流程,让建造者按照标准流程制造。*/class ComputerBuilderDirector {/*** 制造流程1:青春版,不配显示器和键鼠*/public Computer CreateComputer(ComputerBuilder builder) {System.out.println("制造流程1:青春版");builder.setBand();builder.addCpu();builder.addRam();builder.addDisk();builder.addSomethingElse();return builder.building();}/*** 制造流程2:至尊纪念版,配件配齐*/public Computer CreateComputer2(ComputerBuilder builder) {System.out.println("制造流程2:至尊纪念版");builder.setBand();builder.addCpu();builder.addRam();builder.addDisk();builder.addDsiplay();builder.addKeyboard();builder.addMouse();builder.addSomethingElse();return builder.building();}}


3.2、不可变对象(中等)
还是以刚才的制造一台联想电脑为例,联想自家有一套电脑制造的流程和方法,并且要求被制造出来的电脑不允许它再被修改。
先看一下代码,再体会一下不同之处:
public class Main {
public static void main(String[] args) {
Computer computer = new Computer.Builder()
.setBand("联想")
.setCpu("AMD R7 3990x")
.setRam("三星 2*16GB")
.setDisk("三星 EVO 970 NVME 1TB")
.build();
Computer computer1 = new Computer.Builder()
.setBand("联想")
.setCpu("AMD R7 3990x")
.setRam("三星 2*16GB")
.setDisk("三星 EVO 970 NVME 1TB")
.setDisplay("LG 27寸 4K")
.setKeyboard("罗技 MX990 青轴机械键盘")
.setMouse("罗技 G603 无线蓝牙双模鼠标")
.build();
System.out.println(computer);
System.out.println(computer1);
}
}
class Computer {
protected String band;//必须
protected String cpu;//必须
protected String ram;//必须
protected String disk;//必须
protected String keyboard;//可选
protected String mouse;//可选
protected String display;//可选
public Computer(Builder builder) {
this.band = builder.band;
this.cpu = builder.cpu;
this.ram = builder.ram;
this.disk = builder.disk;
this.keyboard = builder.keyboard;
this.mouse = builder.mouse;
this.display = builder.display;
}
public static class Builder {
private String band;//必须
private String cpu;//必须
private String ram;//必须
private String disk;//必须
private String keyboard;//可选
private String mouse;//可选
private String display;//可选
public Computer build() {
// 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
// 检查四个必填项
if (band == null || band.length() == 0) {
// band 不允许为空
throw new IllegalArgumentException("band 不允许为空");
}
if (cpu == null || cpu.length() == 0) {
// cpu 不允许为空
throw new IllegalArgumentException("cpu 不允许为空");
}
if (ram == null || ram.length() == 0) {
// ram 不允许为空
throw new IllegalArgumentException("ram 不允许为空");
}
if (disk == null || disk.length() == 0) {
// disk 不允许为空
throw new IllegalArgumentException("disk 不允许为空");
}
// 特殊条件
if (display != null && display.length() != 0) {
// 如果配了显示器,则鼠标键盘要一起配齐
if (keyboard == null || keyboard.length() == 0) {
// keyboard 不允许为空
throw new IllegalArgumentException("display 已装配,keyboard 不允许为空");
}
if (mouse == null || mouse.length() == 0) {
// mouse 不允许为空
throw new IllegalArgumentException("display 已装配,mouse 不允许为空");
}
}
return new Computer(this);
}
public Builder() {
}
@Override
public String toString() {
return "Builder{" +
"band='" + band + '\'' +
", cpu='" + cpu + '\'' +
", ram='" + ram + '\'' +
", disk='" + disk + '\'' +
", keyboard='" + keyboard + '\'' +
", mouse='" + mouse + '\'' +
", display='" + display + '\'' +
'}';
}
// Builder 的 getter && setter 方法
public String getBand() {
return band;
}
public Builder setBand(String band) {
this.band = band;
return this;
}
public String getCpu() {
return cpu;
}
public Builder setCpu(String cpu) {
this.cpu = cpu;
return this;
}
public String getRam() {
return ram;
}
public Builder setRam(String ram) {
this.ram = ram;
return this;
}
public String getDisk() {
return disk;
}
public Builder setDisk(String disk) {
this.disk = disk;
return this;
}
public String getKeyboard() {
return keyboard;
}
public Builder setKeyboard(String keyboard) {
this.keyboard = keyboard;
return this;
}
public String getMouse() {
return mouse;
}
public Builder setMouse(String mouse) {
this.mouse = mouse;
return this;
}
public String getDisplay() {
return display;
}
public Builder setDisplay(String display) {
this.display = display;
return this;
}
}
@Override
public String toString() {
return "Computer{" +
"band='" + band + '\'' +
", cpu='" + cpu + '\'' +
", ram='" + ram + '\'' +
", disk='" + disk + '\'' +
", keyboard='" + keyboard + '\'' +
", mouse='" + mouse + '\'' +
", display='" + display + '\'' +
'}';
}
public void run() {
System.out.println("电脑启动中");
}
public void showInfo() {
System.out.println("本电脑详细信息:" + this.toString());
}
}
与 3.1 的版本相比,差异还是比较大的。
1)取消了抽象 Builder 建造类
2)将具体的 Builder 建造者类放入了 Computer 电脑类中,作为一个静态内部类使用
3)将 Director 指挥者类取消,将制造流程作为 Builder 的一个方法,让 Builder 自行建造。
lombok 插件的 @Builder 注解直接提供了近似的功能,有兴趣可以自行了解一下。
4、总结
4.1、建造者模式的优缺点
1)优点
创建复杂的对象时,这些对象内部构建间的建造顺序通常比较稳定,但对象内部的构建通常面临着复杂的变化。这时,使用建造者模式可以使得建造代码与表示代码分离,由于建造者隐藏了该产品是如何组装的,所以若需要改变一个产品的内部表示,只需要再定义一个具体的建造者就可以了。
2)缺点(待续)
4.2、关于建造者模式的一些问题
实际上,建造者模式的原理和代码实现非常简单,掌握起来并不难,难点在于应用场景。
比如,你有没有考虑过这样几个问题:
1)直接使用构造函数或者配合 set 方法就能创建对象,为什么还需要建造者模式来创建呢?
2)建造者模式和工厂模式都可以创建对象,那它们两个的区别在哪里呢?
