生成器模式

1 实际场景

1.1 导出数据的应用框架

工厂方法模式一文中,讨论并使用工厂方法模式来解决了导出数据的应用框架中如何选择具体到处方式的问题,并没还有涉及到具体的数据导出实现方式,即,没有实现如何将数据导出成文本、xml 等具体的格式。本文场景需要讨论数据导出的具体实现。

对于导出数据的内容格式具体需求如下:

分为三个部分:文件头、文件体以及文件尾

  • 文件头:一般描述公司编号、导出数据的日期,对于文本格式,中间使用逗号分隔符;
  • 文件体:表名称、粉条描述数据。对于文本格式,标名称单独占一行,数据描述一行算一条数据,字段间用逗号分隔符;
  • 文件尾:描述输出数据的用户信息;

应用框架的其它功能不在重复实现(具体信息可参考工厂方法模式的文章),目前只关注如何实现导出文件,导出格式暂定为文本格式和 xml 格式。

1.2 不使用设计模式的方案

无论导出数据的格式是哪种,导出数据都是相同的。

1、定义数据对象

文件头数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @author mark.hct
* @version ExportHeaderModel.java v 0.1 2023/1/31 23:55 Exp $
* @description 描述输出到文件头的内容的对象
*/
public class ExportHeaderModel {
/**
* 分公司或门市点编号
*/
private String depId;
/**
* 导出数据的日期
*/
private String exportDate;

public String getDepId() {
return depId;
}

public void setDepId(String depId) {
this.depId = depId;
}

public String getExportDate() {
return exportDate;
}

public void setExportDate(String exportDate) {
this.exportDate = exportDate;
}
}

文件体数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* @author mark.hct
* @version ExportDataModel.java v 0.1 2023/1/31 23:56 Exp $
* @description 描述输出数据的对象
*/
public class ExportDataModel {
/**
* 产品编号
*/
private String productId;
/**
* 销售价格
*/
private double price;
/**
* 销售数量
*/
private double amount;

public String getProductId() {
return productId;
}

public void setProductId(String productId) {
this.productId = productId;
}

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

public double getAmount() {
return amount;
}

public void setAmount(double amount) {
this.amount = amount;
}
}

文件尾数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @author mark.hct
* @version ExportFooterModel.java v 0.1 2023/1/31 23:57 Exp $
* @description 描述输出到文件尾的内容的对象
*/
public class ExportFooterModel {
/**
* 输出人
*/
private String exportUser;

public String getExportUser() {
return exportUser;
}

public void setExportUser(String exportUser) {
this.exportUser = exportUser;
}
}

2、文本文件格式导出实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @author mark.hct
* @version ExportToTxt.java v 0.1 2023/2/1 00:00 Exp $
* @description 导出数据到文本文件的对象
*/
public class ExportToTxt {
/**
* 导出数据到文本文件
*
* @param ehm 文件头的内容
* @param mapData 数据的内容
* @param efm 文件尾的内容
*/
public void export(ExportHeaderModel ehm, Map<String, Collection<ExportDataModel>> mapData, ExportFooterModel efm) {
//用来记录最终输出的文件内容
StringBuilder sb = new StringBuilder();
//1:先来拼接文件头的内容
sb.append(ehm.getDepId()).append(",").append(ehm.getExportDate()).append("\n");
//2:接着来拼接文件体的内容
for (String tblName : mapData.keySet()) {
//先拼接表名称
sb.append(tblName).append("\n");
//然后循环拼接具体数据
for (ExportDataModel edm : mapData.get(tblName)) {
sb.append(edm.getProductId()).append(",").append(edm.getPrice()).append(",").append(edm.getAmount()).append("\n");
}
}
//3:接着来拼接文件尾的内容
sb.append(efm.getExportUser());

//为了演示简洁性,这里就不去写输出文件的代码了
//把要输出的内容输出到控制台看看
System.out.println("输出到文本文件的内容:\n" + sb);
}
}

3、xml 格式导出实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* @author mark.hct
* @version ExportToXml.java v 0.1 2023/2/1 00:03 Exp $
* @description 导出数据到XML文件的对象
*/
public class ExportToXml {
/**
* 导出数据到XML文件
*
* @param ehm 文件头的内容
* @param mapData 数据的内容
* @param efm 文件尾的内容
*/
public void export(ExportHeaderModel ehm, Map<String, Collection<ExportDataModel>> mapData, ExportFooterModel efm) {
//用来记录最终输出的文件内容
StringBuilder sb = new StringBuilder();
//1:先来拼接文件头的内容
sb.append("<?xml version='1.0' encoding='gb2312'?>\n");
sb.append("<Report>\n");
sb.append(" <Header>\n");
sb.append(" <DepId>").append(ehm.getDepId()).append("</DepId>\n");
sb.append(" <ExportDate>").append(ehm.getExportDate()).append("</ExportDate>\n");
sb.append(" </Header>\n");
//2:接着来拼接文件体的内容
sb.append(" <Body>\n");
for (String tblName : mapData.keySet()) {
//先拼接表名称
sb.append(" <Datas TableName=\"").append(tblName).append("\">\n");
//然后循环拼接具体数据
for (ExportDataModel edm : mapData.get(tblName)) {
sb.append(" <Data>\n");
sb.append(" <ProductId>").append(edm.getProductId()).append("</ProductId>\n");
sb.append(" <Price>").append(edm.getPrice()).append("</Price>\n");
sb.append(" <Amount>").append(edm.getAmount()).append("</Amount>\n");
sb.append(" </Data>\n");
}
sb.append(" </Datas>\n");
}
sb.append(" </Body>\n");
//3:接着来拼接文件尾的内容
sb.append(" <Footer>\n");
sb.append(" <ExportUser>").append(efm.getExportUser()).append("</ExportUser>\n");
sb.append(" </Footer>\n");
sb.append("</Report>\n");

//为了演示简洁性,这里就不去写输出文件的代码了
//把要输出的内容输出到控制台看看
System.out.println("输出到XML文件的内容:\n" + sb);
}
}

4、客户端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* @author mark.hct
* @version Client.java v 0.1 2023/2/1 00:04 Exp $
* @description
*/
public class Client {
public static void main(String[] args) {
//准备测试数据
ExportHeaderModel ehm = new ExportHeaderModel();
ehm.setDepId("一分公司");
ehm.setExportDate("2010-05-18");

Map<String, Collection<ExportDataModel>> mapData = new HashMap<>();
Collection<ExportDataModel> col = new ArrayList<>();

ExportDataModel edm1 = new ExportDataModel();
edm1.setProductId("产品001号");
edm1.setPrice(100);
edm1.setAmount(80);

ExportDataModel edm2 = new ExportDataModel();
edm2.setProductId("产品002号");
edm2.setPrice(99);
edm2.setAmount(55);
//把数据组装起来
col.add(edm1);
col.add(edm2);
mapData.put("销售记录表", col);

ExportFooterModel efm = new ExportFooterModel();
efm.setExportUser("张三");
//测试输出到文本文件
ExportToTxt toTxt = new ExportToTxt();
toTxt.export(ehm, mapData, efm);
//测试输出到xml文件
ExportToXml toXml = new ExportToXml();
toXml.export(ehm, mapData, efm);
}
}

代码运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
输出到文本文件的内容:
一分公司,2010-05-18
销售记录表
产品001号,100.0,80.0
产品002号,99.0,55.0
张三

输出到XML文件的内容:
<?xml version='1.0' encoding='gb2312'?>
<Report>
<Header>
<DepId>一分公司</DepId>
<ExportDate>2010-05-18</ExportDate>
</Header>
<Body>
<Datas TableName="销售记录表">
<Data>
<ProductId>产品001号</ProductId>
<Price>100.0</Price>
<Amount>80.0</Amount>
</Data>
<Data>
<ProductId>产品002号</ProductId>
<Price>99.0</Price>
<Amount>55.0</Amount>
</Data>
</Datas>
</Body>
<Footer>
<ExportUser>张三</ExportUser>
</Footer>
</Report>

1.3 存在的问题

观察上面的实现,无论输出到哪种格式,步骤基本一致:

  1. 拼接文件头内容;
  2. 拼接文件体内容;
  3. 拼接文件尾内容;
  4. 将拼接好的内容输出;

按照目前的实现方式存在以下问题:

  1. 构建每种输出格式的文件内容的时候,都会重复这几个处理步骤,应该提炼出来,形成公共的处理过程
  2. 今后可能会有很多不同输出格式的要求,这就需要在处理过程不变的情况下,能方便的切换不同的输出格式的处理

即:构建每种格式的数据文件的处理过程,应该和具体的步骤实现分开,这样就能够复用处理过程,而且能很容易的切换不同的输出格式

如何实现?

2 解决方案

2.1 生成器模式

2.1.1 定义

生成器模式(Builder)是创建型模式:将复杂对象的构建与表示分离,是的同样的构建过程可创建不同表示。简单来说就是调用相同的创建对象的方法(建造过程)可以创建出不同的对象。

2.1.2 生成器模式解决方案思路

在上述案例场景中,构建不同格式的数据文件的处理过程,就是对象的构建,而每种格式具体的实现步骤,就相当于不同的表示。因为不同实现步骤,决定了表现的不同。

要实现同样的构建过程可以创建不同的表现,那么一个自然的思路就是先把构建过程独立出来,在生成器模式中把它称为指导者,由它来指导装配过程,但是不负责每步具体的实现。当然,光有指导者是不够的,必须要有能具体实现每步的对象,在生成器模式中称这些实现对象为生成器

如此,指导者就是可以重用的构建过程,而生成器是可以被切换的具体实现。前面的实现中,每种具体的导出文件格式的实现就相当于生成器。

2.1.3 结构说明

image-20230202001114080

Builder:生成器接口,定义创建一个 Product 对象所需的各个部件的操作;

ConcreteBuilder:具体的生成器实现,实现各个部件的创建,并负责组装 Product 对象的各个部件,同时还提供一个让用户获取组装完成后的产品对象的方法;

Direcotor:指导者,主要用来使用 Builder 接口,以一个统一的过程构建所需要的 Product 对象;

Product:产品,表示被创建者构建的复杂对象,包含多个部件;

2.1.4 示例代码

1、生成器接口定义:

1
2
3
4
5
6
7
8
9
10
11
/**
* @author mark.hct
* @version Builder.java v 0.1 2023/2/2 00:44 Exp $
* @description 生成器接口,定义创建一个产品对象所需的各个部件的操作
*/
public interface Builder {
/**
* 示意方法,构建某个部件
*/
void buildPart();
}

2、生成器实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @author mark.hct
* @version ConcreteBuilder.java v 0.1 2023/2/2 00:46 Exp $
* @description 具体的生成器实现对象
*/
public class ConcreteBuilder implements Builder {
/**
* 生成器最终构建的产品对象
*/
private Product resultProduct;

/**
* 获取生成器最终构建的产品对象
*
* @return 生成器最终构建的产品对象
*/
public Product getResult() {
return resultProduct;
}

@Override
public void buildPart() {
//构建某个部件的功能处理
}
}

3、产品对象接口定义:

1
2
3
4
5
6
7
8
/**
* @author mark.hct
* @version Product.java v 0.1 2023/2/2 00:46 Exp $
* @description 被构建的产品对象的接口
*/
public interface Product {
//定义产品的操作
}

4、指导者示意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @author mark.hct
* @version Director.java v 0.1 2023/2/2 00:49 Exp $
* @description 指导者,指导使用生成器的接口来构建产品的对象
*/
public class Director {
/**
* 持有当前需要使用的生成器对象
*/
private Builder builder;

/**
* 构造方法,传入生成器对象
*
* @param builder 生成器对象
*/
public Director(Builder builder) {
this.builder = builder;
}

/**
* 示意方法,指导生成器构建最终的产品对象
*/
public void construct() {
//通过使用生成器接口来构建最终的产品对象
builder.buildPart();
}
}

2.2 重写方案

生成器模式重写实例的结构图:

image-20230202234107504

1、文件头:ExportHeaderModel、文件体:ExportDataModel、文件尾:ExportFooterModel,与前述的定义的一致;

2、Builder 接口定义:将导出各种格式文件的处理过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @author mark.hct
* @version Builder.java v 0.1 2023/2/2 23:47 Exp $
* @description 生成器接口,定义创建一个输出文件对象所需的各个部件的操作
*/
public interface Builder {
/**
* 构建输出文件的Header部分
*
* @param ehm 文件头的内容
*/
void buildHeader(ExportHeaderModel ehm);

/**
* 构建输出文件的Body部分
*
* @param mapData 要输出的数据的内容
*/
void buildBody(Map<String, Collection<ExportDataModel>> mapData);

/**
* 构建输出文件的Footer部分
*
* @param efm 文件尾的内容
*/
void buildFooter(ExportFooterModel efm);
}

3、具体生成器的实现

文本格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* @author mark.hct
* @version TxtBuilder.java v 0.1 2023/2/2 23:50 Exp $
* @description 实现导出数据到文本文件的的生成器对象
*/
public class TxtBuilder implements Builder {
/**
* 用来记录构建的文件的内容,相当于产品
*/
private StringBuilder stringBuilder = new StringBuilder();

@Override
public void buildHeader(ExportHeaderModel ehm) {
stringBuilder.append(ehm.getDepId()).append(",").append(ehm.getExportDate()).append("\n");
}

@Override
public void buildBody(Map<String, Collection<ExportDataModel>> mapData) {
for (String tblName : mapData.keySet()) {
//先拼接表名称
stringBuilder.append(tblName).append("\n");
//然后循环拼接具体数据
for (ExportDataModel edm : mapData.get(tblName)) {
stringBuilder.append(edm.getProductId())
.append(",").append(edm.getPrice())
.append(",").append(edm.getAmount()).append("\n");
}
}
}

@Override
public void buildFooter(ExportFooterModel efm) {
stringBuilder.append(efm.getExportUser());
}

public StringBuilder getResult() {
return stringBuilder;
}
}

xml 格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* @author mark.hct
* @version XmlBuilder.java v 0.1 2023/2/2 23:55 Exp $
* @description 实现导出数据到XML文件的的生成器对象
*/
public class XmlBuilder implements Builder {
/**
* 用来记录构建的文件的内容,相当于产品
*/
private StringBuilder stringBuilder = new StringBuilder();

@Override
public void buildHeader(ExportHeaderModel ehm) {
stringBuilder.append("<?xml version='1.0' encoding='gb2312'?>\n");
stringBuilder.append("<Report>\n");
stringBuilder.append(" <Header>\n");
stringBuilder.append(" <DepId>").append(ehm.getDepId()).append("</DepId>\n");
stringBuilder.append(" <ExportDate>").append(ehm.getExportDate()).append("</ExportDate>\n");
stringBuilder.append(" </Header>\n");
}

@Override
public void buildBody(Map<String, Collection<ExportDataModel>> mapData) {
stringBuilder.append(" <Body>\n");
for (String tblName : mapData.keySet()) {
//先拼接表名称
stringBuilder.append(" <Datas TableName=\"").append(tblName).append("\">\n");
//然后循环拼接具体数据
for (ExportDataModel edm : mapData.get(tblName)) {
stringBuilder.append(" <Data>\n");
stringBuilder.append(" <ProductId>").append(edm.getProductId()).append("</ProductId>\n");
stringBuilder.append(" <Price>").append(edm.getPrice()).append("</Price>\n");
stringBuilder.append(" <Amount>").append(edm.getAmount()).append("</Amount>\n");
stringBuilder.append(" </Data>\n");
}
stringBuilder.append(" </Datas>\n");
}
stringBuilder.append(" </Body>\n");
}

@Override
public void buildFooter(ExportFooterModel efm) {
stringBuilder.append(" <Footer>\n");
stringBuilder.append(" <ExportUser>").append(efm.getExportUser()).append("</ExportUser>\n");
stringBuilder.append(" </Footer>\n");
stringBuilder.append("</Report>\n");
}

public StringBuilder getResult() {
return stringBuilder;
}
}

4、指导者:指导具体的产品构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* @author mark.hct
* @version Director.java v 0.1 2023/2/2 23:59 Exp $
* @description 指导者,指导使用生成器的接口来构建输出的文件的对象
*/
public class Director {
/**
* 持有当前需要使用的生成器对象
*/
private Builder builder;

/**
* 构造方法,传入生成器对象
*
* @param builder 生成器对象
*/
public Director(Builder builder) {
this.builder = builder;
}

/**
* 指导生成器构建最终的输出的文件的对象
*
* @param ehm 文件头的内容
* @param mapData 数据的内容
* @param efm 文件尾的内容
*/
public void construct(ExportHeaderModel ehm, Map<String, Collection<ExportDataModel>> mapData, ExportFooterModel efm) {
//1:先构建Header
builder.buildHeader(ehm);
//2:然后构建Body
builder.buildBody(mapData);
//3:然后构建Footer
builder.buildFooter(efm);
}
}

4、客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* @author mark.hct
* @version Client.java v 0.1 2023/2/3 00:02 Exp $
* @description
*/
public class Client {
public static void main(String[] args) {
//准备测试数据
ExportHeaderModel ehm = new ExportHeaderModel();
ehm.setDepId("一分公司");
ehm.setExportDate("2010-05-18");

Map<String, Collection<ExportDataModel>> mapData = new HashMap<String, Collection<ExportDataModel>>();
Collection<ExportDataModel> col = new ArrayList<ExportDataModel>();

ExportDataModel edm1 = new ExportDataModel();
edm1.setProductId("产品001号");
edm1.setPrice(100);
edm1.setAmount(80);

ExportDataModel edm2 = new ExportDataModel();
edm2.setProductId("产品002号");
edm2.setPrice(99);
edm2.setAmount(55);
//把数据组装起来
col.add(edm1);
col.add(edm2);
mapData.put("销售记录表", col);

ExportFooterModel efm = new ExportFooterModel();
efm.setExportUser("张三");

//测试输出到文本文件
TxtBuilder txtBuilder = new TxtBuilder();
//创建指导者对象
Director director = new Director(txtBuilder);
director.construct(ehm, mapData, efm);
//把要输出的内容输出到控制台看看
System.out.println("输出到文本文件的内容:\n" + txtBuilder.getResult());

//测试输出到xml文件
XmlBuilder xmlBuilder = new XmlBuilder();
Director director2 = new Director(xmlBuilder);
director2.construct(ehm, mapData, efm);
//把要输出的内容输出到控制台看看
System.out.println("输出到XML文件的内容:\n" + xmlBuilder.getResult());
}
}

生成器的优势:对同一构建过程,只要配置不同的生成器实现,就会生成不同表现的对象

3 模式讲解

3.1 认识生成器模式

生成器模式的主要功能是:构建复杂的产品,而且是细化的、分步骤的构建产品,即,生成器模式重在解决一步一步构造复杂对象的问题。更为重要的是,这个构建的过程是统一的,固定不变的,变化的部分放在了生成器部分,只要配置不同的生成器,那么同样的构建过程,就能构建出不同的产品表示出来。

简而言之,生成器模式的重心在于分离构建算法和具体的构造实现,从而使得构建算法可以重用,具体的构造实现可以很方便的拓展课切换,通过灵活的的组合来构造出不同的产品对象。

生成器模式的构成

主要的两部分:

Builder 接口:定义了如何构建各个部件,即,指导每个部件功能如何实现,以及如何装配这些部件到产品中去;

Direcotr:指导如何组合来构建产品,负责整体的构建算法,一般是分步骤来执行

无论模式如何变化,都存在这两个部分,认识这点很重要,因为在生成器模式中,强调的是固定整体构建的算法,而灵活扩展和切换部件的具体构造和产品装配的方式,所以要严格区分这两个部分。

Director 实现整体构建算法的时候,遇到需要创建和组合具体部件的时候,就会把这些功能通过委托,交给 Builder 去完成。

3.2 实现的要点

3.2.1 生成器 Builder

实际上在 Builder 接口的实现中,每个部件构建的方法里面,除了部件装配外,也可以实现如何具体的创建各个部件对象,即,每个方法都可以有两部分功能,一个是创建部件对象,一个是组装部件

在构建部件的方法里面可以实现选择并创建具体的部件对象,然后再把这个部件对象组装到产品对象中去,这样一来,Builder 就可以和工厂方法配合使用

再进一步,如果在实现 Builder 的时候,只有创建对象的功能,而没有组装的功能,那么这个时候 Builder 实现与抽象工厂的实现是类似的。这时 Builder 接口就类似于抽象工厂的接口,Builder 的具体实现就类似于具体的工厂,而且 Builder 接口里面定义的创建各个部件的方法也是有关联的,这些方法是构建一个复杂对象所需要的部件对象。

3.2.2 指导者 Director

在生成器模式里面,指导者承担的是整体构建算法部分,是相对不变的部分。因此在实现指导者的时候,把变化的部分分离出去是很重要的。

其实指导者分离出去的变化部分,就到了生成器那边,指导者知道整体的构建算法,就是不知道如何具体的创建和装配部件对象

因此真正的指导者实现,并不仅仅是如同前面示例那样,简单的按照一定顺序调用生成器的方法来生成对象,并没有这么简单。应该是有较为复杂的算法和运算过程,在运算过程中根据需要,才会调用生成器的方法来生成部件对象

3.2.2 指导者和生成器的交互

在生成器模式里面,指导者和生成器的交互,是通过生成器的 buildPart 方法来完成。在前面的示例中,指导者和生成器是没有太多相互交互的,指导者仅仅只是简单的调用了一下生成器的方法,在实际开发中,比这要复杂。如:

在运行指导者的时候,会按照整体构建算法的步骤进行运算,可能先运行前几步运算,到了某一步骤,需要具体创建某个部件对象了,然后就调用 Builder 中创建相应部件的方法来创建具体的部件。同时,把前面运算得到的数据传递给 Builder,因为在 Builder 内部实现创建和组装部件的时候,可能会需要这些数据;

Builder 创建完具体的部件对象后,会把创建好的部件对象返回给指导者,指导者继续后续的算法运算,可能会用到已经创建好的对象

如此反复下去,直到整个构建算法运行完成。

3.2.3 生成器返回装配好的产品和方法

在标准的生成器模式里面,在 Builder 实现里面会提供一个返回装配好的产品的方法,在 Builder 接口上是没有的。它考虑的是最终的对象一定要通过部件构建和装配,才算真正创建了,而具体干活的就是这个 Builder 实现,虽然指导者也参与了,但是指导者是不负责具体的部件创建和组装的,因此客户端是从 Builder 实现里面获取最终装配好的产品。

3.2.4 被构建产品的接口

在使用生成器模式的时候,大多数情况下是不知道最终构建出来的产品是什么样的,所以在标准的生成器模式里面,一般是不需要对产品定义抽象接口,因为最终构造的产品千差万别,给这些产品定义公共接口几乎是没有意义的。

3.3 生成器模式构建复杂对象

考虑这样一个实际应用,要创建一个保险合同的对象,里面很多属性的值都有约束,要求创建出来的对象是满足这些约束规则的。约束规则比如:保险合同通常情况下可以和个人签订,也可以和某个公司签订,但是一份保险合同不能同时与个人和公司签订。这个对象里面有很多类似这样的约束,该如何来创建这个对象?

要想简洁直观、安全性好、又具有很好的拓展性的来创建这个对象的话,一个很好的选择就是使用 Builder 模式,把复杂的创建过程通过 Buidler 来实现

采用 Builder 模式来构建复杂的对象,通常会对 Builder 模式进行一定的简化,因为目标明确,就是创建某个复杂对象,因此做适当简化会使程序更简洁,大致简化如下:

  1. 由于是用 Builder 模式来创建某个对象,因此就没有必要再定义一个 Builder 接口,直接提供一个具体的构建器类就可以了;
  2. 对于创建一个复杂的对象,可能会有很多种不同的选择和步骤,干脆去掉 “指导者”,把指导者的功能和 Client 的功能合并起来,也就是说,Client 这个时候就相当于指导者,它来指导构建器类去构建需要的复杂对象;

还是来看看示例会比较清楚,为了实例简单,先不去考虑约束的实现,只是考虑如何通过 Builder 模式来构建复杂对象。

1、使用 Builder 模式构建复杂对象(不考虑约束)

(a)保险合同对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* @author mark.hct
* @version InsuranceContract.java v 0.1 2023/2/5 23:07 Exp $
* @description 保险合同的对象
*/
public class InsuranceContract {
/**
* 保险合同编号
*/
private String contractId;
/**
* 被保险人员的名称,同一份保险合同,要么跟人员签订,要么跟公司签订,
* 也就是说,"被保险人员"和"被保险公司"这两个属性,不可能同时有值
*/
private String personName;
/**
* 被保险公司的名称
*/
private String companyName;
/**
* 保险开始生效的日期
*/
private long beginDate;
/**
* 保险失效的日期,一定会大于保险开始生效的日期
*/
private long endDate;
/**
* 示例:其它数据
*/
private String otherData;

/**
* 构造方法,访问级别是同包能访问
*/
InsuranceContract(ConcreteBuilder builder) {
this.contractId = builder.getContractId();
this.personName = builder.getPersonName();
this.companyName = builder.getCompanyName();
this.beginDate = builder.getBeginDate();
this.endDate = builder.getEndDate();
this.otherData = builder.getOtherData();
}

/**
* 示意:保险合同的某些操作
*/
public void someOperation() {
System.out.println("Now in Insurance Contract someOperation==" + this.contractId);
}
}

注:构造方法传入的是构建器对象,里面包含有所有保险合同需要的数据。

(b)构造器的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* @author mark.hct
* @version ConcreteBuilder.java v 0.1 2023/2/5 23:08 Exp $
* @description 构造保险合同对象的构建器
*/
public 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;
}

/**
* 选填数据,被保险人员的名称
*
* @param personName 被保险人员的名称
* @return 构建器对象
*/
public ConcreteBuilder setPersonName(String personName) {
this.personName = personName;
return this;
}

/**
* 选填数据,被保险公司的名称
*
* @param companyName 被保险公司的名称
* @return 构建器对象
*/
public ConcreteBuilder setCompanyName(String companyName) {
this.companyName = companyName;
return this;
}

/**
* 选填数据,其它数据
*
* @param otherData 其它数据
* @return 构建器对象
*/
public ConcreteBuilder setOtherData(String otherData) {
this.otherData = otherData;
return this;
}

/**
* 构建真正的对象并返回
*
* @return 构建的保险合同的对象
*/
public InsuranceContract build() {
return new InsuranceContract(this);
}

public String getContractId() {
return contractId;
}

public String getPersonName() {
return personName;
}

public String getCompanyName() {
return companyName;
}

public long getBeginDate() {
return beginDate;
}

public long getEndDate() {
return endDate;
}

public String getOtherData() {
return otherData;
}
}

注:构建器提供了类似于 setter 的方法,来供外部设置需要的参数。每个这种方法都有返回值,返回的是构建器对象,这样客户端就可以通过连缀(链式)的方式来使用 Builder,以创建他们需要的对象。

(c)客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author mark.hct
* @version Client.java v 0.1 2023/2/5 23:09 Exp $
* @description
*/
public class Client {
public static void main(String[] args) {
//创建构建器
ConcreteBuilder builder = new ConcreteBuilder("001", 12345L, 67890L);
//设置需要的数据,然后构建保险合同对象
InsuranceContract contract = builder.setPersonName("张三").setOtherData("test").build();
//操作保险合同对象的方法
contract.someOperation();
}
}

2、 使用 Builder 模式构建复杂对象(考虑约束)

带约束规则构建复杂对象,大致的实现步骤与刚才的实现一直,只是需要在刚才的实现上把约束规则添加上去。

通常有两个地方可以添加约束规则:

  1. 在构建器的每一个类似于 setter 的方法,可以在这里进行单个数据的约束规则校验,如果不正确,就抛出 IllegalStateException
  2. 在构建器的 build 方法,在创建保险合同对象之前,对所有的数据都可以进行数据的约束规则校验,尤其是那些涉及到几个数据之间的约束关系,在这里校验会比较合适。如果不正确,同样抛出 IllegalStateException

新的 builder 方法示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 构建真正的对象并返回
* @return 构建的保险合同的对象
*/
public InsuranceContract 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==false && signCompany==false){
throw new IllegalArgumentException("一份保险合同不能没有签订对象");
}
if(beginDate<=0){
throw new IllegalArgumentException("合同必须有保险开始生效的日期");
}
if(endDate<=0){
throw new IllegalArgumentException("合同必须有保险失效的日期");
}
if(endDate<=beginDate){
throw new IllegalArgumentException("保险失效的日期必须大于保险生效日期");
}

return new InsuranceContract(this);
}

3 合并构建对象与被构建对象

在实际开发中,如果构建器对象和被构建的对象是这样分开的话,可能会导致同包内的对象不使用构建器来构建对象,而是直接去使用 new 来构建对象,进而会导致错误;另外,这个构建器的功能就是为了创建被构建的对象,完全可以不用单独一个类。

对于这种情况,重构的手法通常是将类内联化(Inline Class),即,就是把构建器对象合并到被构建对象里面去

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/**
* @author mark.hct
* @version InsuranceContract.java v 0.1 2023/2/5 23:24 Exp $
* @description
*/
public class InsuranceContract {
private String contractId;
private String personName;
private String companyName;
private long beginDate;
private long endDate;
private String otherData;

/**
* 构造方法,访问级别是私有的
*/
private InsuranceContract(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 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;
}

/**
* 选填数据,被保险人员的名称
*
* @param personName 被保险人员的名称
* @return 构建器对象
*/
public ConcreteBuilder setPersonName(String personName) {
this.personName = personName;
return this;
}

/**
* 选填数据,被保险公司的名称
*
* @param companyName 被保险公司的名称
* @return 构建器对象
*/
public ConcreteBuilder setCompanyName(String companyName) {
this.companyName = companyName;
return this;
}

/**
* 选填数据,其它数据
*
* @param otherData 其它数据
* @return 构建器对象
*/
public ConcreteBuilder setOtherData(String otherData) {
this.otherData = otherData;
return this;
}

/**
* 构建真正的对象并返回
*
* @return 构建的保险合同的对象
*/
public InsuranceContract 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 == false && signCompany == false) {
throw new IllegalArgumentException("一份保险合同不能没有签订对象");
}
if (beginDate <= 0) {
throw new IllegalArgumentException("合同必须有保险开始生效的日期");
}
if (endDate <= 0) {
throw new IllegalArgumentException("合同必须有保险失效的日期");
}
if (endDate <= beginDate) {
throw new IllegalArgumentException("保险失效的日期必须大于保险生效日期");
}

return new InsuranceContract(this);
}
}

/**
* 示意:保险合同的某些操作
*/
public void someOperation() {
System.out.println("Now in Insurance Contract someOperation==" + this.contractId);
}
}

通过上面的示例可以看出,这种实现方式会更简单和直观。此时客户端的写法也发生了一点变化,主要就是创建构造器的地方需要变化,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author mark.hct
* @version Client.java v 0.1 2023/2/5 23:25 Exp $
* @description
*/
public class Client {
public static void main(String[] args) {
//创建构建器
InsuranceContract.ConcreteBuilder builder = new InsuranceContract.ConcreteBuilder("001", 12345L, 67890L);
//设置需要的数据,然后构建保险合同对象
InsuranceContract contract = builder.setPersonName("张三").setOtherData("test").build();
//操作保险合同对象的方法
contract.someOperation();
}
}

3.4 生成器模式的优缺点

1、 松散耦合

生成器模式可以用同一个构建算法,构建出表现上完全不同的产品,实现产品构建和产品表现上的分离。生成器模式正是把产品构建的过程独立出来,使它和具体产品的表现松散耦合,从而使得构建算法可以复用,而具体产品表现也可以灵活的、方便的拓展和切换。

2、灵活低改变产品的内部表示

在生成器模式中,由于 Builder 对象只是提供接口给 Director 使用,那么具体的部件创建和装配方式是被 Builder 接口隐藏,Director 并不知道这些具体的实现细节。这样一来,要想改变产品的内部表示,只需要切换 Builder 的具体实现即可,不用管 Director

3、更高的复用性

生成器模式很好实现了构建算法和具体产品实现的分离,这样一来,使得构建产品的算法可以复用。同样的道理,具体产品的实现也可以复用,同一个产品的实现,可以配合不同的构建算法使用。

3.5 思考

3.5.1 生成器模式的本质

本质:分离整体构建算法和部件构造。

构建一个复杂的对象,本来就有构建的过程,以及构建过程中具体的实现,生成器模式就是用来分离这两个部分,从而使得程序结构更松散、扩展更容易、复用性更好,同时也会使得代码更清晰,意图更明确。

虽然在生成器模式的整体构建算法中,会一步一步引导 Builder 来构建对象,但这并不是说生成器就主要是用来实现分步骤构建对象的。生成器模式的重心还是在于分离整体构建算法和部件构造,而分步骤构建对象不过是整体构建算法的一个简单表现,或者说是一个附带产物

3.5.2 使用场景

  1. 如果创建对象的算法,应该独立于该对象的组成部分以及它们的装配方式时;
  2. 如果同一个构建过程有着不同的表示时;

3.6 相关模式

3.6.1 生成器模式与工厂方法模式

这两个模式可以组合使用。生成器模式的 Builder 实现中,通常需要选择具体的部件实现,一个可行的方案就是实现成为工厂方法,通过工厂方法来获取具体的部件对象,然后再进行部件的装配。

3.6.2 生成器模式与抽象工厂模式

这两个模式既相似又有区别,也可以组合使用。

区别:

抽象工厂模式的主要目的是创建产品簇,这个产品簇里面的单个产品,就相当于是构成一个复杂对象的部件对象,抽象工厂对象创建完成过后就立即返回整个产品簇;而生成器模式的主要目的是按照构造算法,一步一步来构建一个复杂的产品对象,通常要等到整个构建过程结束过后,才会得到最终的产品对象。

事实上,这两个模式是可以组合使用,在生成器模式的 Builder 实现中,需要创建各个部件对象,而这些部件对象是有关联的,通常是构成一个复杂对象的部件对象,也就是说,Builder 实现中,需要获取构成一个复杂对象的产品簇,那自然就可以使用抽象工厂模式来实现。这样一来,由抽象工厂模式负责了部件对象的创建,Builder 实现里面就主要负责产品对象整体的构建。

3.6.3 生成器模式与模板方法模式

这也是两个非常类似的模式

模板方法模式主要是用来定义算法的骨架,把算法中某些步骤延迟到子类中实现。而生成器模式,Director 用来定义整体的构建算法,把算法中某些涉及到具体部件对象的创建和装配的功能,委托给具体的 Builder 来实现。

虽然生成器不是延迟到子类,是委托给 Builder,但那只是具体实现方式上的差别,从实质上看两个模式很类似,都是定义一个固定的算法骨架,然后把算法中的某些具体步骤交给其它类来完成,都能实现整体算法步骤和某些具体步骤实现的分离。

具体区别:

目的:生成器模式是用来构建复杂对象的,而模板方法是用来定义算法骨架,尤其是一些复杂的业务功能的处理算法的骨架;

实现:生成器模式是采用委托的方法,而模板方法是采用的继承的方式;

使用的复杂度:生成器模式需要组合 DirectorBuilder 对象,然后才能开始构建,要等构建完后才能获得最终的对象,而模板方法就没有这么麻烦,直接使用子类对象即可。

3.6.4 生成器模式与组合模式

两个模式可以组合使用,对于复杂的组合结构,可以使用生成器模式一步一步构建。