Skip to main content

生成器模式 (Builder)

场景问题​

问题案例​

我们开发的业务系统,很常见的一个功能就是业务数据导出成各种文件格式,比如:文本、Excel、PDF、Word、Xml等。

对于导出数据的应用框架,通常对于具体的导出内容和格式是有要求的,假如现在有如下的要求,简单描述一下:

  • 导出的文件,不管什么格式,都分成3个部分,分别是文件头、文件体和文件尾。
  • 在文件头部分,需要描述如下信息:分公司或门市点编号、导出数据的日期,对于文本格式,中间用逗号分隔。
  • 在文件体部分,需要描述如下信息:表名称,然后分条描述数据。对于文本格式,表名称单独占一行,数据描述一行算一条数据,字段间用逗号分隔。
  • 在文件尾部分,需要描述如下信息:输出人。

现在就要来实现上述功能。为了演示简单点,在工厂方法模式里面已经实现的功能,就再不重复了,这里只关心如何实现导出文件,而且只实现导出成文本格式和Xml格式就可以了,其他就不用考虑了。

不用模式的解决方案​

不就是要实现导出数据到文本文件和XML文件吗?其实不管什么格式,需要导出的数据是一样的,只是具体导出到文件中的内容则会随着格式的不同而不同。

  1. 下面将描述文件各个部分的数据对象定义出来。 先来看看描述输出到文件头的内容的对象。示例代码如下:
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("张三");

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

}
}

/**
* 描述输出到文件头的内容的对象
*/
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;
}
}

/**
* 描述输出数据的对象
*/
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;
}

}

/**
* 描述输出到文件尾的内容的对象
*/
public class ExportFooterModel {
/**
* 输出人
*/
private String exportUser;

public String getExportUser() {
return exportUser;
}

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

}

/**
* 导出数据到文本文件的对象
*/
public class ExportToTxt {
/**
* 导出数据到文本文件
* @param ehm 文件头的内容
* @param mapData 数据的内容
* @param efm 文件尾的内容
*/
public void export(ExportHeaderModel ehm,Map<String,Collection<ExportDataModel>> mapData,ExportFooterModel efm){
//用来记录最终输出的文件内容
StringBuffer buffer = new StringBuffer();
//1:先来拼接文件头的内容
buffer.append(ehm.getDepId()+","+ehm.getExportDate()+"\n");
//2:接着来拼接文件体的内容
for(String tblName : mapData.keySet()){
//先拼接表名称
buffer.append(tblName+"\n");
//然后循环拼接具体数据
for(ExportDataModel edm : mapData.get(tblName)){
buffer.append(edm.getProductId()+","+edm.getPrice()+","+edm.getAmount()+"\n");
}
}
//3:接着来拼接文件尾的内容
buffer.append(efm.getExportUser());

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

/**
* 导出数据到XML文件的对象
*/
public class ExportToXml {
/**
* 导出数据到XML文件
* @param ehm 文件头的内容
* @param mapData 数据的内容
* @param efm 文件尾的内容
*/
public void export(ExportHeaderModel ehm,Map<String,Collection<ExportDataModel>> mapData,ExportFooterModel efm){
//用来记录最终输出的文件内容
StringBuffer buffer = new StringBuffer();
//1:先来拼接文件头的内容
buffer.append("<?xml version='1.0' encoding='gb2312'?>\n");
buffer.append("<Report>\n");
buffer.append(" <Header>\n");
buffer.append(" <DepId>"+ehm.getDepId()+"</DepId>\n");
buffer.append(" <ExportDate>"+ehm.getExportDate()+"</ExportDate>\n");
buffer.append(" </Header>\n");
//2:接着来拼接文件体的内容
buffer.append(" <Body>\n");
for(String tblName : mapData.keySet()){
//先拼接表名称
buffer.append(" <Datas TableName=\""+tblName+"\">\n");
//然后循环拼接具体数据
for(ExportDataModel edm : mapData.get(tblName)){
buffer.append(" <Data>\n");
buffer.append(" <ProductId>"+edm.getProductId()+"</ProductId>\n");
buffer.append(" <Price>"+edm.getPrice()+"</Price>\n");
buffer.append(" <Amount>"+edm.getAmount()+"</Amount>\n");
buffer.append(" </Data>\n");
}
buffer.append(" </Datas>\n");
}
buffer.append(" </Body>\n");
//3:接着来拼接文件尾的内容
buffer.append(" <Footer>\n");
buffer.append(" <ExportUser>"+efm.getExportUser()+"</ExportUser>\n");
buffer.append(" </Footer>\n");
buffer.append("</Report>\n");

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

有何问题​

仔细观察上面的实现,会发现,不管是输出成文本文件,还是输出到XML文件,在实现的时候,步骤基本上都是一样的,大致分成了以下四步。

  1. 先拼接文件头的内容。
  2. 然后拼接文件体的内容。
  3. 再拼接文件尾的内容。
  4. 最后把拼接好的内容输出去成为文件。

也就是说,对于不同的输出格式,处理步骤是一样的,但是每步的具体实现是不一样的。按照现在的实现方式,就存在如下的问题。

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

换句话来说,也就是构建每种格式的数据文件的处理过程,应该和具体的步骤实现分开,这样就能够复用处理过程,而且能很容易地切换不同地输出格式。 可是该如何实现呢?

解决方案​

用来解决上述问题的一个合理的解决方案就是生成器模式。那么什么是生成器模式呢?

生成器模式的定义​

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

生成器模式的解决思路​

仔细分析上面的实现,构建每种格式的数据文件的处理过程,这不就是构建过程吗?而每种格式具体的步骤实现,不就相当于是不同的表示吗?因为不同的步骤实现,决定了最终的表现也就不同。也就是说,上面的问题恰好就是生成器模式要解决的问题。 要实现同样的构建过程可以创建不同的表现,那么一个自然的思路就是先把构建过程独立出来,在生成器模式中把它称为指导者,由它来指导装配过程,但是不负责每步具体的实现。当然,光有指导者是不够的,必须要有能具体实现每步的对象,在生成器模式中称这些实现对象为生成器。 这样一来,指导者就是可以重用的构建过程,而生成器是可以被切换的具体实现。前面的实现中,每种具体的导出文件格式的实现就相当于生成器。

生成器模式的结构​

cd-builder.svg

  • Builder:生成器接口,定义创建一个Product对象所需的各个部件的操作。
  • ConcreteBuilder:具体的生成器实现,实现各个部件的创建,并负责组装Product对象的各个部件,同时还提供一个让用户获取组装完成后的产品对象的方法。
  • Director:指导者,也称导演类,负责安排已有模块的顺序,然后告诉生成器开始构建。
  • Product:产品,表示被生成器构建的复杂对象。ConcreteBuilder创建该产品的内部表示并定义它的装配过程。

生成器模式的实现​

/**
* 构建器接口,定义创建一个产品对象所需的各个部件的操作
*/
public interface Builder {
/**
* 示意方法,构建某个部件
*/
public void buildPart();
}

/**
* 具体的构建器实现对象
*/
public class ConcreteBuilder implements Builder {
/**
* 构建器最终构建的产品对象
*/
private Product resultProduct;
/**
* 获取构建器最终构建的产品对象
* @return 构建器最终构建的产品对象
*/
public Product getResult() {
return resultProduct;
}
public void buildPart() {
//构建某个部件的功能处理
}
}

/**
* 被构建的产品对象的接口
*/
public interface Product {
//定义产品的操作
}

/**
* 指导者,指导使用构建器的接口来构建产品的对象
*/
public class Director {
/**
* 持有当前需要使用的构建器对象
*/
private Builder builder;
/**
* 构造方法,传入构建器对象
* @param builder 构建器对象
*/
public Director(Builder builder) {
this.builder = builder;
}
/**
* 示意方法,指导构建器构建最终的产品对象
*/
public void construct() {
//通过使用构建器接口来构建最终的产品对象
builder.buildPart();
}
}

生成器重写示例​

要使用生成器模式来重写示例,重要的任务就是要把指导者和生成器接口定义出来。指导者就是用来执行那四个步骤的对象,而生成器是用来实现每种格式下,对于每个步骤的具体实现的对象。

示例结构​

design-builder-1.svg

示例代码​

/**
* 构建器接口,定义创建一个输出文件对象所需的各个部件的操作
*/
public interface Builder {
/**
* 构建输出文件的Header部分
* @param ehm 文件头的内容
*/
public void buildHeader(ExportHeaderModel ehm);
/**
* 构建输出文件的Body部分
* @param mapData 要输出的数据的内容
*/
public void buildBody(Map<String,Collection<ExportDataModel>> mapData);
/**
* 构建输出文件的Footer部分
* @param efm 文件尾的内容
*/
public void buildFooter(ExportFooterModel efm);
}

/**
* 实现导出数据到文本文件的的构建器对象
*/
public class TxtBuilder implements Builder {
/**
* 用来记录构建的文件的内容,相当于产品
*/
private StringBuffer buffer = new StringBuffer();
public void buildBody(
Map<String, Collection<ExportDataModel>> mapData) {
for(String tblName : mapData.keySet()){
//先拼接表名称
buffer.append(tblName+"\n");
//然后循环拼接具体数据
for(ExportDataModel edm : mapData.get(tblName)){
buffer.append(edm.getProductId()+","+edm.getPrice()+","+edm.getAmount()+"\n");
}
}
}
public void buildFooter(ExportFooterModel efm) {
buffer.append(efm.getExportUser());
}
public void buildHeader(ExportHeaderModel ehm) {
buffer.append(ehm.getDepId()+","+ehm.getExportDate()+"\n");
}
public StringBuffer getResult(){
return buffer;
}
}

/**
* 实现导出数据到XML文件的的构建器对象
*/
public class XmlBuilder implements Builder {
/**
* 用来记录构建的文件的内容,相当于产品
*/
private StringBuffer buffer = new StringBuffer();
public void buildBody(
Map<String, Collection<ExportDataModel>> mapData) {
buffer.append(" <Body>\n");
for(String tblName : mapData.keySet()){
//先拼接表名称
buffer.append(" <Datas TableName=\""+tblName+"\">\n");
//然后循环拼接具体数据
for(ExportDataModel edm : mapData.get(tblName)){
buffer.append(" <Data>\n");
buffer.append(" <ProductId>"+edm.getProductId()+"</ProductId>\n");
buffer.append(" <Price>"+edm.getPrice()+"</Price>\n");
buffer.append(" <Amount>"+edm.getAmount()+"</Amount>\n");
buffer.append(" </Data>\n");
}
buffer.append(" </Datas>\n");
}
buffer.append(" </Body>\n");
}

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

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

}

/**
* 指导者,指导使用构建器的接口来构建输出的文件的对象
*/
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);
}
}

/**
* 描述输出到文件头的内容的对象
*/
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;
}
}

/**
* 描述输出数据的对象
*/
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;
}

}

/**
* 描述输出到文件尾的内容的对象
*/
public class ExportFooterModel {
/**
* 输出人
*/
private String exportUser;

public String getExportUser() {
return exportUser;
}

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

}

客户端调用

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());

}
}

模式讲解​

生成器模式的功能​

生成器模式的主要功能是构建复杂的产品,而且是细化的、分步骤的构建产品,也就是生成器模式重在一步一步解决构造复杂对象的问题。如果仅仅这么认识生成器模式的功能是不够的。

更为重要的是,这个构建的过程是统一的、固定不变的,变化的部分放到生成器部分了,只要配置不同的生成器,那么同样的构建过程,就能构建出不同的产品来。

再直白点说,生成器模式的重心在于分离构建算法和具体的构造实现,从而使得构建算法可以重用。具体的构造实现可以很方便地扩展和切换,从而可以灵活地组合来构造出不同的产品对象。

生成器模式的构成​

要特别注意,生成器模式分成两个很重要的部分。

  • 一个部分是Builder接口,这里是定义了如何构建各个部件,也就是知道每个部件功能如何实现,以及如何装配这些部件到产品中去;
  • 另外一个部分是Director,Director是知道如何组合来构建产品,也就是说Director负责整体的构建算法,而且通常是分步骤地来执行。

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

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

生成器模式的使用​

应用生成器模式的时候,可以让客户端创造Director,在Director里面封装整体构建算法,然后让Director去调用Builder,让Builder来封装具体部件的构建功能,这就如同前面的例子。

还有一种退化的情况,就是让客户端和Director融合起来,让客户端直接去操作Builder,就好像是指导者自己想要给自己构建产品一样。

执行时序图​

design-builder-2.svg