Java设计模式之Builder模式

在这里我暂时这样理解,就是把复杂对象的构建过程和结果进行分离 也就是解耦

Builder模式概念

The builder pattern is an object creation software design pattern. Unlike the abstract factory pattern and the factory method pattern whose intention is to enable polymorphism, the intention of the builder pattern is to find a solution to the telescoping constructor anti-pattern[citation needed]. The telescoping constructor anti-pattern occurs when the increase of object constructor parameter combination leads to an exponential list of constructors. Instead of using numerous constructors, the builder pattern uses another object, a builder, that receives each initialization parameter step by step and then returns the resulting constructed object at once. (代替构造函数)

The builder pattern has another benefit. It can be used for objects that contain flat data (html code, SQL query, X.509 certificate…), that is to say, data that can’t be easily edited. This type of data cannot be edited step by step and must be edited at once. The best way to construct such an object is to use a builder class.(创建不可变对象)

定义
原文 – 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. (from wikipedia)
译文 – 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

定义有点抽象,先来看个Builder的一个比较直观的使用场景 —— 使用Builder代替具有多个参数(4个以上)的构造函数

场景简介

如一个类表示包装食品外面显示的营养成分标签,标签中有些域是必须的,不过有些也是可选域。大多数产品在某几个可选域中都有非0值。对于这样的类,一般都使用重叠构造器(telescoping sconstructor)模式。这种模式将会出现过个构造函数,创建实例的时候选择合适的构造函数。例如如下代码:

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
// Telescoping constructor pattern - does not scale well!
public class NutritionFacts {
private final int servingSize; //(ml) required
private final int servings; //(per container) required
private final int calories; // optional
private final int fat; // optional
private final int sodium; // optional
privatefinalintcarbohydrate; // optional
public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}

调用的时候使用NutritionFacts nut = new NutritionFacts(xx, xx, xx, xx, 0, xx); 这样的话可能就存在你不想给sodium赋值,但是你又得给carbohydrate赋值,就导致你必须给sodium传入0。如果参数数目更多的话,这种情况更糟糕,难以控制,而且当你传参的顺序弄错时,并不会提示错误,但运行可能会报错,对别人的可读性也不高。

JavaBeans模式

先来看第一种解决方案 —— JavaBeans模式

JavaBeans模式是调用一个无参构造函数创建对象,然后调用属性的setter方法对每个属性赋值。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// JavaBeans Pattern - allows inconsistency, mandates mutability
public class NutritionFacts {
// Parameters initialized to default values (if any)
private int servingSize = -1; // Required; no default value
private int servings=-1;//""""
private int calories=0;
private int fat=0;
private int sodium=0;
private int carbohydrate = 0;
public NutritionFacts() { }
// Setters 汉子
public void setServingSize(int val) { servingSize = val; }
public void setServings(int val) { servings = val; }
public void setCalories(int val) { calories = val; }
public void setFat(int val) { fat = val; }
public void setSodium(int val) { sodium = val; }
public void setCarbohydrate(int val) { carbohydrate = val; }
}

这个代码比使用重叠构造函数的代码简单了点,阅读也较为容易,但是调用方法为:

1
2
3
4
5
6
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

这种模式弥补了重叠构造函数的不足,使代码简单,可阅读;但是在调用时要先new一个对象,而无法保证我们在使用这个对象之前已经对相应的属性进行赋值。

JavaBeans模式的缺点是:使对象的构建过程分到几个调用中,在构造过程中可能处于不一致的状态。类无法仅仅通过检验构造器参数的有效性来保证一致性。试图使用处于不一致状态的对象,将会导致失败。这种失败与包含错误的代码大相劲庭,因为其调试起来十分困难。JavaBeans不能创建不可变的实例,因为需要你付出额外的代价来保证这个类是线程安全的。

Builder模式

第二种解决方案 —— Builder模式

既能保证向重叠构造器模式那样的安全性,也能保证向JavaBeans模式那么好的可读性。这就是Builde模式的一种形式。不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器或者静态工厂,得到一个builder对象。然后客户端在builder上调用类似setter的方法来设置每个相关的可选参数。最后客户端调用无参的build方法来生成不可变的对象。这个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
30
31
32
33
34
35
36
37
38
39
40
41
42
// Builder Pattern
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
private int calories = 0;
private int fat = 0;
private int carbohydrate = 0;
private int sodium = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val)
{ calories = val; return this; }
public Builder fat(int val)
{ fat = val; return this; }
public Builder carbohydrate(int val)
{ carbohydrate = val; return this; }
public Builder sodium(int val)
{ sodium = val; return this; }
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}

调用时是这样的:
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();

就像构造器那样,builder 可以强加给它的参数变量(这个强加的参数变量可以作为创建对象的必须参数),build 方法实例化实体类,实例化前,可以对参数进行检查,如果不满足条件的,应该抛出 IllegalStateException,或者其他自定义异常。
另外一个小优点是 builder 可以有多个可变参数。构造器,像方法一样,可能只有一个可变参数。因为 builder 用不同的方法设置参数,只要你喜欢,他们可以有很多可变参数。

代码中NutritionFacts类中属性都是final类型的,这是必须的呢还是只是为了让对象实例一旦创建,其状态就不能在改变。而JavaBeans却无法创建这种不可变对象。

与JavaBeans的区别其实是在于真正构造对象的时间点上。其实Builder并没有提前把对象构造出来然后再一个个地对参数进行设置,而是先设定值,再在最后的build()方法中构建出对象。这样在一些参数存在依赖关系的时候,可以很好地解决依赖的问题。当然,对于参数的传递也不一定要按照上面写的方式,Setter其实也是可以的,其思想不在于模拟按名字传递参数,在于后面的build()。

从上面的例子中可以看出,Builder模式多数是用于对象比较复杂,可以逐步去构建的时候,其核心在于将类的构建逻辑转移到类的实例化外部。其经典应用是从一段文本中构建对象,因为文本的读入是以流的形式,那么一开始的时候可能没有办法创建完整的目标对象,这时候可以使用构建者模式来进行构建。但是在注重性能的环境下,每次创建实例都必须先创建Builder构造器也会是一笔开销,而且代码也有点冗长。总的来说如果类有多个属性需要多个构造函数,Builder模式还是一个不错的选择。

Builder部件解析

再回到Builder的定义,将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。在这里我暂时这样理解,就是把复杂对象的构建过程和结果进行分离。Builder的uml图为:
Builder uml图
有点类似工厂模式,其中

  • Builder:生成器接口,定义创建一个Product对象所需要的各个部件的操作。
  • ConcreteBuilder:具体的生成器实现,实现各个部件的创建,并负责组装Product对象的各个部件,同时还提供一个让用户获取组装完成后的产品对象的方法。
  • Director:指导者,也被称导向者,主要用来使用Builder接口,以一个统一的过程来构建所需要的Product对象。
  • Product:产品,表示被生成器构建的复杂对象,包含多个部件。

看了上面的uml图和各个部件的解释,可能就会产生疑问了,上面的使用Builder代替构造函数的场景中使用的角色和UML中的角色不对称了,不要着急,下面进行解释:

上面的使用场景比较明确,就是创建某个复制对象,可以进行适当的简化。其中Builder模式只是用来创建某个对象,则就没有必要定义Builder的接口,直接提供一个具体的ConcreteBuilder就行;如果只创建一个复杂的对象,不可能会有很多种不同的选择和步骤,导演类Director就可以干掉了,将其功能与Client的功能合并,使Client具备Director的功能,来指导构建器来构建需要的复杂对象。

Builder模式的功能

生成器模式的主要功能是构建复杂的产品,而且是细化的,分步骤的构建产品,也就是生成器模式重在一步一步解决构造复杂对象的问题。如果仅仅这么认知生成器模式的功能是不够的。
更为重要的是,这个构建的过程是统一的、固定不变的,变化的部分放到生成器部分了,只要配置不同的生成器,那么同样的构建过程,就能构建出不同的产品来。 这是对定义(将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示 )更进一步的解释。

正常的Builder模式的代码示例如下:
1、生成器接口定义的示例代码

1
2
3
4
5
6
7
8
9
10
11
/**
* 生成器接口,定义创建一个产品对象所需的各个部件的操作
*
*/
public interface Builder {
/**
* 示意方法,构建某个部件
*/
public 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
/**
* 具体的生成器实现对象
*
*/
public class ConcreteBuilder implements Builder {
private Product resultProduct;
/**
* 获取生成器最终构建的产品对象
* @return
*/
public Product getResultProduct() {
return resultProduct;
}
@Override
public void buildPart() {
//构建某个部件的功能处理
}
}

3、相应的产品对象接口的示例代码

1
2
3
4
5
6
7
/**
* 被构建的产品对象的接口
*
*/
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
/**
* 指导者,指导使用生成器的接口来构建产品对象
*
*/
public class Director {
/**
* 持有当前需要使用的生成器对象
*/
private Builder builder;
/**
* 构造方法,传人生成器对象
* @param builder
*/
public Director(Builder builder) {
this.builder = builder;
}
/**
* 示意方法,指导生成器构建最终的产品对象
*/
public void construct(){
//通过使用生成器接口来构建最终的产品对象
builder.buildPart();
}
}

应用场景– 导出数据的应用框架

在讨论工厂方法模式的时候,提供了一个导出数据的应用框架。
对于导出数据的应用框架,通常在导出数据上,会有一些约束的方式,比如导出成文本格式、数据库备份形式、Excel格式、Xml格式等。
在工厂方法模式章节里面,讨论并使用工厂方法模式来解决了如何选择具体导出方式的问题,并没有涉及到每种方式具体如何实现。
换句话说,在讨论工厂方法模式的时候,并没有讨论如何实现导出成文本、Xml等具体格式,本章就来讨论这个问题。
对于导出数据的应用框架,通常对于具体的导出内容和格式是有要求的,加入现在有如下要求,简单描述一下:

  • 导出的文件,不管是什么格式,都分成3个部分,分别是文件头、文件体、文件尾。
  • 在文件头部分,需要描述如下信息:分公司或者门市编号、导出数据的日期。
  • 在文件体部分,需要描述如下信息:表名称,然后分条描述数据。
  • 在文件尾部分,需要描述如下信息:输出人。

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
/**
* 描述输出到文件头的内容的对象
*
*/
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
/**
* 描述输出数据的对象
*
*/
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
20
/**
* 描述输出到文件尾的内容的对象
*
*/
public class ExportFooterModel {
/**
* 输出人
*/
private String exportUser;
public String getExportUser() {
return exportUser;
}
public void setExportUser(String exportUser) {
this.exportUser = exportUser;
}
}

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
/**
* 生成器接口,定义创建一个输出文件对象所需的各个部件的操作
*
*/
public interface Builder {
/**
* 构建输出文件的Header部分
* @param ehm
*/
public void buildHeader(ExportHeaderModel ehm);
/**
* 构建输出文件的Body部分
* @param mapData
*/
public void buildBody(Map<String,List<ExportDataModel>> mapData);
/**
* 构建输出文件的Footer部分
* @param efm
*/
public 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
/**
* 实现导出文件到文本文件的生成器对象
*
*/
public class TxtBuilder implements Builder {
/**
* 用来记录构建的文件的内容,相当于产品
*/
private StringBuffer buffer = new StringBuffer();
@Override
public void buildHeader(ExportHeaderModel ehm) {
buffer.append(ehm.getDepId()+","+ehm.getExportDate()+"\n");
}
@Override
public void buildBody(Map<String, List<ExportDataModel>> mapData) {
for(String tablName : mapData.keySet()){
//先拼接表名
buffer.append(tablName+"\n");
//然后循环拼接具体数据
for(ExportDataModel edm : mapData.get(tablName)){
buffer.append(edm.getProductId()+","+edm.getPrice()+","+edm.getAmount()+"\n")
}
}
}
@Override
public void buildFooter(ExportFooterModel efm) {
buffer.append(efm.getExportUser());
}
public StringBuffer getResult(){
return buffer;
}
}

导出到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
53
54
55
56
57
58
/**
* 实现导出文件到Xml文件的生成器对象
*
*/
public class XmlBuilder implements Builder {
/**
* 用来记录构建的文件的内容,相当于产品
*/
private StringBuffer buffer = new StringBuffer();
@Override
public void buildHeader(ExportHeaderModel ehm) {
buffer.append("<?xml version='1.0' encoding='UTF-8'?>\n");
buffer.append("<Report>\n");
buffer.append("\t<Header>\n");
buffer.append("\t\t<DepId>"+ehm.getDepId()+"</DepId>\n");
buffer.append("\t\t<ExportDate>"+ehm.getExportDate()+"</ExportDate>\n");
buffer.append("\t</Header>\n");
}
@Override
public void buildBody(Map<String, List<ExportDataModel>> mapData) {
buffer.append("\t<Body>\n");
for(String tablName : mapData.keySet()){
//先拼接表名
buffer.append("\t\t<Datas TableName=\""+tablName+"\">\n");
//然后循环拼接具体数据
for(ExportDataModel edm : mapData.get(tablName)){
buffer.append("\t\t\t<Data>\n");
buffer.append("\t\t\t\t<ProductId>"+edm.getProductId()+"</ProductId>\n");
buffer.append("\t\t\t\t<Price>"+edm.getPrice()+"</Price>\n");
buffer.append("\t\t\t\t<Amount>"+edm.getAmount()+"</Amount>\n");
buffer.append("\t\t\t</Data>\n");
}
buffer.append("\t\t</Datas>\n");
}
buffer.append("\t</Body>\n");
}
@Override
public void buildFooter(ExportFooterModel efm) {
buffer.append("\t<Footer>\n");
buffer.append("\t\t<ExportUser>"+efm.getExportUser()+"</ExportUser>\n");
buffer.append("\t</Footer>\n");
buffer.append("</Report>\n");
}
public StringBuffer getResult(){
return buffer;
}
}

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
/**
* 指导者,指导使用生成器的接口来构建输出的文件对象
*
*
*/
public class Director {
/**
* 持有当前需要的使用的生成器对象
*/
private Builder builder;
/**
* 构造方法,传入生成器对象
*
* @param builder
*/
public Director(Builder builder) {
this.builder = builder;
}
public void construct(ExportHeaderModel ehm,
Map<String, List<ExportDataModel>> mapData, ExportFooterModel efm) {
//1.先构建Header
builder.buildHeader(ehm);
//2.然后构建Body
builder.buildBody(mapData);
//3.再构建Footer
builder.buildFooter(efm);
}
}

5、客户端测试代码如下:

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
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
//准备测试数据
ExportHeaderModel ehm = new ExportHeaderModel();
ehm.setDepId("一分公司");
ehm.setExportDate("2010-05-18");
Map<String, List<ExportDataModel>> mapData = new HashMap<String, List<ExportDataModel>>
List<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(120);
edm2.setAmount(280);
ExportDataModel edm3 = new ExportDataModel();
edm3.setProductId("产品003号");
edm3.setPrice(320);
edm3.setAmount(380);
col.add(edm1);
col.add(edm2);
col.add(edm3);
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("输出到文本文件的内容:"+txtBuilder.getResult().toString());
XmlBuilder xmlBuilder = new XmlBuilder();
Director director2 = new Director(xmlBuilder);
director2.construct(ehm, mapData, efm);
//把要输出的内容输出到控制台看看
System.out.println("输出到Xml文件的内容:"+xmlBuilder.getResult().toString());
}
}

生成器模式的功能

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

##参考
http://blog.csdn.net/top_code/article/details/8469297
https://en.wikipedia.org/wiki/Builder_pattern

您的肯定,是我装逼的最大的动力!