前言
建造模式是对象的创建模式。建造模式可以将一个产品的内部表象(internal representation
)与产品的生产过程分割开来,从而可以使一个建造过程生成具有不同的内部表象的产品对象。
(一). 产品的内部表象
一个产品常有不同的组成成分作为产品的零件,这些零件有可能是对象,也有可能不是对象,他们通常又称为产品的内部表象(internal representation
)。
(二). 对象性质的建造
有些情况下,一个对象会有些重要的性质,在它们没有正确赋值之前,对象不能作为一个完整的产品使用。比如:一个电子邮件有发件人地址、收件人地址、主题、内容、附件等部分,而在最基本的发件人地址得到赋值之前,这个电子邮件是不可以发送的。
有些情况下,一个对象的有些性质必须按照某个顺序赋值才有意义。在某个性质没有赋值之前,另一个性质则无法赋值。
这些情况使得性质本身的建造设计到复杂的业务逻辑。设置后,此对象相当于一个有待建造的产品,而对象的这些性质相当于产品的零件,建造产品的过程是建造零件的过程。
由于建造零件的过程很复杂,因此,这些零件的建造过程往往被外部化到另一个成为建造者的对象中,建造者对象返还给客户端的是一个全部零件都建造完毕的产品对象。
建造模式利用一个导演者对象和具体建造者对象一个个的建造出所有的零件,从而建造出完整的产品对象。建造者模式将产品的结构和产品的零件的建造过程对客户端隐藏起来,把对建造过程进行指挥的责任和具体建造者零件的责任分割开来,达到责任划分和封装的目的。
正文
建造模式的结构
在这个示意的系统里,最终产品Product
只有两个零件,即part1
和part2
。相应的构造方法也有两个,即buildPart1()
和buildPart2()
。
同时可以看出本模式涉及到四个角色,它们分别为:
抽象建造者(Builder):
给出一个抽象接口,以规范产品对象的各个组成成分的建造。模式中真正创建产品对象的是具体建造者ConcreteBuilder
角色。
具体建造者类必须实现这个接口要求的两种方法:
1. 一种是产品具体零件建造方法:buildPart1()
和buildPart2()
;
2. 另一种是返回构造完成的产品的方法retrieveResult()
。
一般来说,产品所包含的零件数目与建造方法的数目相符。换言之,有多少零件需要建造,就会有多少相应的建造方法。
具体建造者(ContreteBuilder):
担任这个角色的是抽象建造者在具体业务场景的下的建造实现。这个角色要完成的任务包括:
- 实现抽象建造者
Builder
所声明的接口,给出一步步完成创建产品实例的操作。 - 在建造过程完成后,提供产品的实例。
导演者(Director):
担任这个角色的类调用具体建造者角色以创建产品对象。应当指出的是,导演者角色并没有产品类的具体知识,真正拥有产品类的具体知识的是具体建造者角色。
产品(Product):
产品便是建造中的复杂对象,一般来说,一个系统中会有多于一个的产品类,而且这些产品类并不一定有共同的接口,而完全可以是不相关联的。
建造模式的示例代码
Product.java
public class Product {
/**
* 产品零件
*/
private String part1;
private String part2;
public String getPart1() {
return part1;
}
public void setPart1(String part1) {
this.part1 = part1;
}
public String getPart2() {
return part2;
}
public void setPart2(String part2) {
this.part2 = part2;
}
@Override
public String toString() {
return "Product [part1=" + part1 + ", part2=" + part2 + "]";
}
}
Builder.java
/**
* 抽象建造者角色
*
* 提供零件建造方法及返回结果方法
*/
public interface Builder {
void buildPart1();
void buildPart2();
Product retrieveResult();
}
ConcreteBuilder.java
/**
* 具体建造者角色
*/
public class ConcreteBuilder implements Builder {
private Product product = new Product();
/**
* 建造零件1
*/
@Override
public void buildPart1() {
product.setPart1("零件分类1,编号:10000");
}
/**
* 建造零件2
*/
@Override
public void buildPart2() {
product.setPart2("零件分类2,编号:20000");
}
/**
* 返回建造后成功的产品
* @return
*/
@Override
public Product retrieveResult() {
return product;
}
}
Director.java
/**
* 导演者角色
*/
public class Director {
/**
* 创建建造者对象
*/
private Builder builder;
/**
* 构造函数,给定建造者对象
* @param builder 建造者对象
*/
public Director(Builder builder) {
this.builder = builder;
}
/**
* 产品构造方法,在该方法内,调用产品零件建造方法。
*/
public Product construct(){
builder.buildPart1();
builder.buildPart2();
// 返回builder建造完成的产品对象
return builder.construct();
}
}
Client.java
public class Client {
public static void main(String[] args) {
//创建具体建造者对象
Builder builder = new ConcreteBuilder();
//创造导演者角色,给定建造者对象
Director director = new Director(builder);
//调用导演者角色,创建产品零件。并返回产品建造结果。
Product product = director.construct();
System.out.println(product);
}
}
上述代码完成的具体步骤:
1. 客户端创建具体建造者对象;
2. 将具体建造者对象交给导演者;
3. 导演者操作建造者对象建造产品零件;
4. 当产品创建完成后,导演者将产品返回给客户端。
建造者模式构建复杂对象
考虑这样一个实际业务应用,要创建一个保险合同的对象,里面很多属性的值都有约束,要求创建出来的对象是满足这些约束规则的。
约束规则如下:
保险合同通常情况下可以和个人签订,也可以和某个公司签订个,但是一份保险合同不能同时和个人和公司签订。这个对象里有很多类似于这样的约束,采用建造者模式来构建复杂的对象,通常会对建造者模式进行一定的简化,因为目标明确,就是创建某个复杂对象,因此做适当的简化会使得程序更简介。
具体实现思路如下:
- 由于是用
Builder
建造者模式来创建某个对象,因此就没有必要再定义一个Builder
接口,直接提供一个具体的建造类就可以了。 - 对于创建一个复杂的对象,可能会有很多种不同的选择和步骤,干脆去掉导演者
Director
,把导演者的功能和Client
客户端的功能合并起来,也就是说Client
客户端的功能就相当于导演者,它来指导建造者去构建需要的复杂对象。
于是,建造者(Builder
)可以抽象到目标产品(Product
)的内部,这样最大的好处对外屏蔽掉具体的建造实现,是示例代码如下:
InstranceContract.java
/**
* 保险合同编号
*/
public class InstranceContract {
/**
* 保险合同编号
*/
private String contractId;
/**
* 受保人名称,此处因为有限制条件:要么同个人签订,要么同公司签订
* 也就是说,受保人名称属性同受保公司名称属性不能同时有值。
*/
private String personName;
/**
* 受保公司名称
*/
private String companyName;
/**
* 开始时间
*/
private long beginDate;
/**
* 结束时间,需要大于开始时间
*/
private long endDate;
/**
* 其他数据
*/
private String otherData;
private InstranceContract(ConcreteBuilder builder){
this.contractId = builder.contractId;
this.personName = builder.personName;
this.companyName = builder.companyName;
this.beginDate = builder.beginDate;
this.endDate = builder.endDate;
this.otherData = builder.otherData;
}
/**
* 保险合同的一些操作
*/
public void someOperation(){
System.out.println("当前正在操作的保险合同编号为【"+this.contractId+"】");
System.out.println(this);
}
@Override
public String toString() {
return "InstranceContract [contractId=" + contractId +
", personName=" + personName +
", companyName="+ companyName +
", beginDate=" + beginDate +
", endDate=" + endDate +
", otherData=" + otherData +
"]";
}
public static class ConcreteBuilder {
private String contractId;
private String personName;
private String companyName;
private long beginDate;
private long endDate;
private String otherData;
/**
* 构造方法
* @param contractId 保险合同编号
* @param beginDate 生效时间
* @param endDate 失效时间
*/
public ConcreteBuilder(String contractId, long beginDate, long endDate) {
this.contractId = contractId;
this.beginDate = beginDate;
this.endDate = endDate;
}
public ConcreteBuilder setPersonName(String personName) {
this.personName = personName;
return this;
}
public ConcreteBuilder setCompanyName(String companyName) {
this.companyName = companyName;
return this;
}
public ConcreteBuilder setOtherData(String otherData) {
this.otherData = otherData;
return this;
}
public InstranceContract build() {
if (contractId == null || contractId.trim().length() == 0) {
throw new IllegalArgumentException("合同编号不能为空");
}
boolean signPerson = (personName != null && personName.trim().length() > 0);
boolean signCompany = (companyName != null && companyName.trim().length() > 0);
if (signPerson && signCompany) {
throw new IllegalArgumentException("一份保险合同不能同时与个人和公司签订");
}
if (!signPerson && !signCompany) {
throw new IllegalArgumentException("一份保险合同不能没有签订对象");
}
if (beginDate <= 0) {
throw new IllegalArgumentException("一份保险合同必须有生效的日期");
}
if (endDate <= 0) {
throw new IllegalArgumentException("一份保险合同必须有失效的日期");
}
if (endDate <= beginDate) {
throw new IllegalArgumentException("一份保险合同的失效日期必须要大于生效的日期");
}
return new InstranceContract(this);
}
}
}
客户端(Client
)、导演者(Director
)合并到一个类上面,如下:
public class Client {
public static void main(String[] args) {
InstranceContract.ConcreteBuilder builder =
new InstranceContract.ConcreteBuilder("8888", 1233L, 2253L);
// 导演者进行组装
InstranceContract contract =
builder.setPersonName("赵四").setOtherData("测试数据").build();
contract.someOperation();
}
}
总结
建造者模式主要适用于如下的业务场景:
- 内部结构复杂:
需要生成的产品对象有复杂的内部结构,每一个内部组件本身也可以是复杂对象,也可以仅仅是一个简单的组成部分。
- 属性顺序和依赖:
需要生成的产品对象的属性相互依赖。建造模式可以强制实行一种分步骤顺序进行的建造过程。因此,如果产品对象的一个属性必须在另外一个属性赋值之后才可以被赋值,那么,使用建造者模式是一个很好的设计思想。
- 属性获取过程复杂:
在对象创建过程中会使用到系统中的一些其他对象,这些对象在产品对象的创建过程中不易得到。
欢迎关注技术公众号: 零壹技术栈
本帐号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。