设计模式4-建造者模式
建造者模式的定义
建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计出模式属于创建型模式,它提供了一种创建对象的最佳方式。
一个Builder类会一步一步构造最终的对象。该Builder类是独立于其他对象的。
举例
电脑是由CPU、主板、内存、硬盘、显卡、机箱、显示器、键盘、鼠标等部件组装而成的复杂产品,卖家一般不会自己去组装电脑,而是将电脑的各种配置要求告诉销售公司,销售公司安排专业的技术人员进行电脑的组装,并将组装好的电脑交付卖家。
意图
将一个复杂的构建与其表示相分离,使得同样的构造过程可以创建不同的表示
主要解决
主要解决在软件系统中,有时候面临着“一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
何时使用
一些基本部件不会变,而其组合经常变化的时候。
如何解决
将变与不变分离开。
关键代码
- 建造者:创建和提供实例;
- 导演:管理建造出来的实例的依赖关系;
优点
- 封装性好,构建和表示分离。
- 扩展性好,各个具体的建造者相互独立,有利于系统的解耦。
- 客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险。
缺点
- 产品的组成部分必须相同,这限制了其使用范围。
- 如内部变化复杂,会有很多的建造类
- 如果产品的内部变化复杂,如果产品内部发生变化,则建造者也要同步修改,后期维护成本较大。
使用场景
- 相同的方法,不同的执行顺序,产生不同的结果。
- 多个部件或零件,都可以装配到一个对象中,但是产生的结果又不相同。
- 产品类非常复杂,或者产品类中不同的调用顺序产生不同的作用。
- 初始化一个对象特别复杂,参数多,而且很多参数都具有默认值。
注意事项
- 与工厂模式的区别是:
- 建造者模式更加注重方法的调用顺序,工厂模式注重创建对象。
- 创建对象的力度不同,建造者模式创建复杂的对象,由各种复杂的部件组成,工厂模式创建出来的对象都一样。
- 关注重点不一样,工厂模式只需要把对象创建出来就可以了,而建造者模式不仅要创建出对象,还要知道对象由哪些部件组成。
- 建造者模式根据建造过程中的顺序不一样,最终对象部件组成也不一样。
- 应用过程中可以根据需要改变,如果创建的产品种类只有一种,只需要一个具体建造者,这时可以省略掉抽象建造者,甚至可以省略掉指挥者角色。
建造者模式的结构
结构图如下:
主要角色:
- 产品(Product):包含多个组成部件的复杂对象,由具体建造者来创建其各个零部件。
- 建造者抽象(Builder):是一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法getResult()。
- 具体建造者(ConcreteBuilder):实现Builder接口,完成复杂产品的各个部件的具体创建方法。
- 调用者(Director):调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。
建造者模式的实现
下面以组装电脑演示建造者模式。
首先有一个电脑类(产品)
/**
* 产品-电脑
*
* @author ccheng
* @date 2021/12/19
*/
@Data
public class Computer {
/**
* cpu
*/
private String cpu;
/**
* 显卡
*/
private String graphics;
/**
* 内存
*/
private String memory;
/**
* 硬盘
*/
private String disk;
@Override
public String toString() {
return "电脑{" +
"cpu='" + cpu + '\'' +
", 显卡='" + graphics + '\'' +
", 内存='" + memory + '\'' +
", 硬盘='" + disk + '\'' +
'}';
}
}
然后有一个抽象的建造者-电脑组装工人
/**
* 抽象建造者-电脑组装工人
*
* @author ccheng
* @date 2021/12/19
*/
@Slf4j
public abstract class AbstractComputerBuilder {
/**
* 具体产品
*/
protected Computer computer = new Computer();
/**
* 选择CPU
*/
abstract void selectCpu(String cpu);
/**
* 选择显卡
*/
abstract void selectGraphics(String graphics);
/**
* 选择内存
*/
abstract void selectMemory(String memory);
/**
* 选择硬盘
*/
abstract void selectDisk(String disk);
/**
* 组装电脑
*
* @return
*/
public Computer build() {
log.info("开始组装电脑......");
log.info("第一步:安装CPU:" + computer.getCpu());
log.info("第二步:安装内存:" + computer.getMemory());
log.info("第三步:安装显卡:" + computer.getGraphics());
log.info("第四步:安装硬盘:" + computer.getDisk());
log.info("第五步:安装电源、显示器、键盘、鼠标......");
log.info("组装电脑完成:" + computer);
return computer;
}
}
然后有品牌组装机的建造者,品牌组装机配置固定不能变
/**
* 抽象建造者-联想品牌电脑的组装工人
* 固定配置:英特尔 i5-11600k cpu + 核显 + 金士顿 8g 内存 + 西部数据蓝盘 1t 硬盘
*
* @author ccheng
* @date 2021/12/19
*/
@Slf4j
public class LenovoComputerBuilder extends AbstractComputerBuilder{
private static final String DEFAULT_CPU = "英特尔 i5-11600k";
private static final String DEFAULT_GRAPHICS = "核显";
private static final String DEFAULT_MEMORY = "金士顿 8g";
private static final String DEFAULT_DISK = "西部数据蓝盘 1t";
public LenovoComputerBuilder() {
computer.setCpu(DEFAULT_CPU);
computer.setGraphics(DEFAULT_GRAPHICS);
computer.setMemory(DEFAULT_MEMORY);
computer.setDisk(DEFAULT_DISK);
}
@Override
void selectCpu(String cpu) {
log.info("固定配置不可更改");
}
@Override
void selectMemory(String memory) {
log.info("固定配置不可更改");
}
@Override
void selectGraphics(String graphics) {
log.info("固定配置不可更改");
}
@Override
void selectDisk(String disk) {
log.info("固定配置不可更改");
}
}
也可以自己DIY配置
/**
* 抽象建造者-自定义配置的组装工人
*
* @author ccheng
* @date 2021/12/19
*/
@Slf4j
public class CustomComputerBuilder extends AbstractComputerBuilder {
@Override
void selectCpu(String cpu) {
computer.setCpu(cpu);
log.info("客户选择了{} cpu", cpu);
}
@Override
void selectMemory(String memory) {
computer.setMemory(memory);
log.info("客户选择了{} 内存", memory);
}
@Override
void selectGraphics(String graphics) {
computer.setGraphics(graphics);
log.info("客户选择了{} 显卡", graphics);
}
@Override
void selectDisk(String disk) {
computer.setDisk(disk);
log.info("客户选择了{} 硬盘", disk);
}
}
还有一个电脑店老板,指挥工人干活
/**
* 指挥者-电脑店老板
*
* @author ccheng
* @date 2021/12/19
*/
@Slf4j
public class ShopBoss {
public Computer getComputer(AbstractComputerBuilder builder, double fee) {
if (fee <= 0) {
log.info("想打劫?给个毛线你");
return null;
} else {
log.info("收到{}元,开始干活吧", fee);
return builder.build();
}
}
}
最后客户可以选择买品牌电脑,也可以自己自定义电脑的配置
/**
* 客户
*
* @author ccheng
* @date 2021/12/19
*/
@Slf4j
public class Client {
public static void main(String[] args) {
//电脑店老板
ShopBoss shopBoss = new ShopBoss();
//买一台联想品牌组装机
AbstractComputerBuilder lenovoBuilder = new LenovoComputerBuilder();
Computer lenovoComputer = shopBoss.getComputer(lenovoBuilder, 4000);
log.info("买到联想品牌组装机:{}", lenovoComputer);
log.info("##########################################");
//买一台DIY组装机
AbstractComputerBuilder customBuilder = new CustomComputerBuilder();
customBuilder.selectCpu("英特尔 i7-11700k");
customBuilder.selectGraphics("英伟达 P1000");
customBuilder.selectMemory("金士顿DDR5 16g");
customBuilder.selectDisk("三星980 500g");
Computer computerComputer = shopBoss.getComputer(customBuilder, 8000);
log.info("买到DIY组装机:{}", computerComputer);
}
}
//运行结果
收到4000.0元,开始干活吧
开始组装电脑......
第一步:安装CPU:英特尔 i5-11600k
第二步:安装内存:金士顿 8g
第三步:安装显卡:核显
第四步:安装硬盘:西部数据蓝盘 1t
第五步:安装电源、显示器、键盘、鼠标......
组装电脑完成:电脑{cpu='英特尔 i5-11600k', 显卡='核显', 内存='金士顿 8g', 硬盘='西部数据蓝盘 1t'}
买到联想品牌组装机:电脑{cpu='英特尔 i5-11600k', 显卡='核显', 内存='金士顿 8g', 硬盘='西部数据蓝盘 1t'}
##########################################
客户选择了英特尔 i7-11700k cpu
客户选择了英伟达 P1000 显卡
客户选择了金士顿DDR5 16g 内存
客户选择了三星980 500g 硬盘
收到8000.0元,开始干活吧
开始组装电脑......
第一步:安装CPU:英特尔 i7-11700k
第二步:安装内存:金士顿DDR5 16g
第三步:安装显卡:英伟达 P1000
第四步:安装硬盘:三星980 500g
第五步:安装电源、显示器、键盘、鼠标......
组装电脑完成:电脑{cpu='英特尔 i7-11700k', 显卡='英伟达 P1000', 内存='金士顿DDR5 16g', 硬盘='三星980 500g'}
买到DIY组装机:电脑{cpu='英特尔 i7-11700k', 显卡='英伟达 P1000', 内存='金士顿DDR5 16g', 硬盘='三星980 500g'}
整个过程,客户不需要知道电脑是怎么组装的,但是整个电脑配置都是客户可以自己选择的,也就是构造与表示分离。电脑店老板也不关心建造的细节,他只负责指挥。
当然根据应用的不同,可以省略掉抽象建造者,甚至可以省略掉指挥者角色。同时为了使用方便,我们还会加上链式编程。
比如说电脑店老板刚创业,还请不起工人,这个时候老板自己又是建造者又是指挥者,我们对上面的建造者类和指挥者类进行合并,电脑类不变。
/**
* 电脑组装者(又是工人,又是老板)
*
* @author ccheng
* @date 2021/12/19
*/
@Slf4j
public class ComputerBuilder {
/**
* 默认配置方案
*/
private static final String DEFAULT_CPU = "英特尔 i5-11600k";
private static final String DEFAULT_GRAPHICS = "核显";
private static final String DEFAULT_MEMORY = "金士顿 8g";
private static final String DEFAULT_DISK = "西部数据蓝盘 1t";
/**
* 具体产品
*/
protected Computer computer = new Computer();
/**
* 选择CPU
*/
ComputerBuilder selectCpu(String cpu) {
computer.setCpu(cpu);
//链式编程,返回this
return this;
}
/**
* 选择显卡
*/
ComputerBuilder selectMemory(String memory) {
computer.setMemory(memory);
return this;
}
/**
* 选择内存
*/
ComputerBuilder selectGraphics(String graphics) {
computer.setGraphics(graphics);
return this;
}
/**
* 选择硬盘
*/
ComputerBuilder selectDisk(String disk) {
computer.setDisk(disk);
return this;
}
/**
* 组装电脑
*
* @return
*/
public Computer build() {
log.info("开始组装电脑......");
//如果客户没有自定义配置,使用默认配置
if (computer.getCpu() == null) {
computer.setCpu(DEFAULT_CPU);
}
if (computer.getGraphics() == null) {
computer.setGraphics(DEFAULT_GRAPHICS);
}
if (computer.getMemory() == null) {
computer.setMemory(DEFAULT_MEMORY);
}
if (computer.getDisk() == null) {
computer.setDisk(DEFAULT_DISK);
}
log.info("第一步:安装CPU:" + computer.getCpu());
log.info("第二步:安装内存:" + computer.getMemory());
log.info("第三步:安装显卡:" + computer.getGraphics());
log.info("第四步:安装硬盘:" + computer.getDisk());
log.info("第五步:安装电源、显示器、键盘、鼠标......");
log.info("组装电脑完成:" + computer);
return computer;
}
}
客户端使用变化不大,依据是客户选择配置,建造者根据配置组装成品,不过由于加入链式编程,调用过程变得更加简单易懂
/**
* 客户
*
* @author ccheng
* @date 2021/12/19
*/
@Slf4j
public class Client {
public static void main(String[] args) {
ComputerBuilder builder = new ComputerBuilder();
Computer computer = builder.selectCpu("英特尔 i7-11700k")
.selectGraphics("英伟达 P1000")
.selectMemory("金士顿DDR5 16g")
.selectDisk("三星980 500g")
.build();
log.info("买到DIY组装机:{}", computer);
}
}
//运行结果
开始组装电脑......
第一步:安装CPU:英特尔 i7-11700k
第二步:安装内存:金士顿DDR5 16g
第三步:安装显卡:英伟达 P1000
第四步:安装硬盘:三星980 500g
第五步:安装电源、显示器、键盘、鼠标......
组装电脑完成:电脑{cpu='英特尔 i7-11700k', 显卡='英伟达 P1000', 内存='金士顿DDR5 16g', 硬盘='三星980 500g'}
买到DIY组装机:电脑{cpu='英特尔 i7-11700k', 显卡='英伟达 P1000', 内存='金士顿DDR5 16g', 硬盘='三星980 500g'}
源码中的建造者模式
jdk中的java.lang.StringBuilder类,它提供了各种重载的append方法,给我们开放构造步骤,最后调用toString()方法就可以获得一个构造好的完整字符串。
还有spring中的BeanDefinitionBuilder,mybatis中的CacheBuilder等