【创建型模式五】生成器(Builder)

1 场景问题#

1.1 继续导出数据的应用框架##

在讨论工厂方法模式的时候,提到了一个导出数据的应用框架。

对于导出数据的应用框架,通常在导出数据上,会有一些约定的方式,比如导出成:文本格式、数据库备份形式、Excel格式、Xml格式等等。

在工厂方法模式章节里面,讨论并使用工厂方法模式来解决了如何选择具体导出方式的问题,并没有涉及到每种方式具体如何实现。换句话说,在讨论工厂方法模式的时候,并没有讨论如何实现导出成文本、Xml等具体的格式,本章就来讨论这个问题。

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

导出的文件,不管什么格式,都分成三个部分,分别是文件头、文件体和文件尾

在文件头部分,需要描述如下信息:分公司或门市点编号、导出数据的日期,对于文本格式,中间用逗号分隔

在文件体部分,需要描述如下信息:表名称、然后分条描述数据。对于文本格式,表名称单独占一行,数据描述一行算一条数据,字段间用逗号分隔。

在文件尾部分,需要描述如下信息:输出人

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

1.2 不用模式的解决方案##

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

  1. 先来把描述文件各个部分的数据对象定义出来,先看描述输出到文件头的内容的对象,示例代码如下:
/**
   * 描述输出到文件头的内容的对象
   */
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;
    }
}
  1. 接下来具体的看看导出的实现,先看导出数据到文本文件的对象,主要就是要实现拼接输出的内容,示例代码如下:
/**
   * 导出数据到文本文件的对象
   */
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);
    }
}
  1. 接下来看看导出数据到XML文件的对象,比较麻烦,要按照XML的格式进行拼接,示例代码如下:
/**
   * 导出数据到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);
    }
}
  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);
    }
}

运行结果如下:

运行结果

1.3 有何问题##

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

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

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

  1. 构建每种输出格式的文件内容的时候,都会重复这几个处理步骤,应该提炼出来,形成公共的处理过程

  2. 今后可能会有很多不同输出格式的要求,这就需要在处理过程不变的情况下,能方便的切换不同的输出格式的处理

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

可是该如何实现呢?

2 解决方案#

2.1 生成器模式来解决##

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

  1. 生成器模式定义
生成器模式定义
  1. 应用生成器模式来解决的思路

仔细分析上面的实现,构建每种格式的数据文件的处理过程,这不就是构建过程吗?而每种格式具体的步骤实现,不就相当于是不同的表示吗?因为不同的步骤实现,决定了最终的表现也就不同。也就是说,上面的问题恰好就是生成器模式要解决的问题。

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

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

2.2 模式结构和说明##

生成器模式的结构如图所示:

生成器模式的结构

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

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

Director:指导者,也被称为导向者,主要用来使用Builder接口,以一个统一的过程来构建所需要的Product对象。

Product:产品,表示被生成器构建的复杂对象,包含多个部件。

2.3 生成器模式示例代码##

  1. 先看看生成器的接口定义,示例代码如下:
/**
   * 生成器接口,定义创建一个产品对象所需的各个部件的操作
   */
public interface Builder {
    /**
     * 示意方法,构建某个部件
     */
    public void buildPart();
}
  1. 再看看具体的生成器实现,示例代码如下:
/**
   * 具体的生成器实现对象
   */
public class ConcreteBuilder implements Builder {
    /**
     * 生成器最终构建的产品对象
     */
    private Product resultProduct;
    /**
     * 获取生成器最终构建的产品对象
     * @return 生成器最终构建的产品对象
     */
    public Product getResult() {
       return resultProduct;
    }

    public void buildPart() {
       //构建某个部件的功能处理
    }
}
  1. 看看相应的产品对象的接口示意,示例代码如下:
/**
   * 被构建的产品对象的接口
   */
public interface Product {
    //定义产品的操作
}
  1. 再来看看指导者的实现示意,示例代码如下:
/**
   * 指导者,指导使用生成器的接口来构建产品的对象
   */
public class Director {
    /**
     * 持有当前需要使用的生成器对象
     */
    private Builder builder;

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

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

2.4 使用生成器模式重写示例##

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

按照生成器模式重写示例的结构如图所示:

按照生成器模式重写示例的结构
  1. 前面示例中的三个数据模型对象还继续沿用,这里就不去赘述了。

  2. 先来看看定义的Builder接口,主要是把导出各种格式文件的处理过程的步骤定义出来,每个步骤负责构建最终导出文件的一部分。示例代码如下:

/**
   * 生成器接口,定义创建一个输出文件对象所需的各个部件的操作
   */
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);
}
  1. 接下来看看具体的生成器实现,其实就是把原来示例中,写在一起的实现,分拆成多个步骤实现了,先看看导出数据到文本文件的生成器实现,示例代码如下:
/**
   * 实现导出数据到文本文件的的生成器对象
   */
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文件的生成器实现,示例代码如下:

/**
   * 实现导出数据到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;
    }
}
  1. 指导者

有了具体的生成器实现后,需要有指导者来指导它进行具体的产品构建,由于构建的产品是文本内容,所以就不用单独定义产品对象了。示例代码如下:

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

       //测试输出到文本文件
       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 认识生成器模式##

  1. 生成器模式的功能

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

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

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

  1. 生成器模式的构成

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

一个部分是Builder接口这边,这边是定义了如何构建各个部件,也就是知道每个部件功能如何实现,以及如何装配这些部件到产品中去。

另外一个部分是Director这边,Director是知道如何组合来构建产品,也就是说Director负责整体的构建算法,而且通常是分步骤的来执行。

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

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

  1. 生成器模式的使用

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

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

  1. 生成器模式的调用顺序示意图
生成器模式的调用顺序示意图

3.2 生成器模式的实现##

  1. 生成器的实现

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

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

再进一步,如果在实现Builder的时候,只有创建对象的功能,而没有组装的功能,那么这个时候的Builder实现跟抽象工厂的实现是类似的

这种情况下,Builder接口就类似于抽象工厂的接口,Builder的具体实现就类似于具体的工厂,而且Builder接口里面定义的创建各个部件的方法也是有关联的,这些方法是构建一个复杂对象所需要的部件对象,仔细想想,是不是非常类似呢。

  1. 指导者的实现

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

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

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

  1. 指导者和生成器的交互

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

指导者通常会实现比较复杂的算法或者是运算过程,在实际中很可能会有这样的情况:

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

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

如此反复下去,直到整个构建算法运行完成,那么最终的产品对象也就创建好了;

通过上面的描述,可以看出指导者和生成器是需要交互的,方式就是通过生成器方法的参数和返回值,来回的传递数据。事实上,指导者是通过委托的方式来把功能交给生成器去完成

  1. 返回装配好的产品的方法

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

当然在Java里面,我们也可以把这个方法添加到Builder接口里面。

  1. 关于被构建的产品的接口

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

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

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

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

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

由于是用Builder模式来创建某个对象,因此就没有必要再定义一个Builder接口,直接提供一个具体的构建器类就可以了;

对于创建一个复杂的对象,可能会有很多种不同的选择和步骤,干脆去掉“指导者”,把指导者的功能和Client的功能合并起来,也就是说,Client这个时候就相当于指导者,它来指导构建器类去构建需要的复杂对象;

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

  1. 使用Builder模式来构建复杂对象,先不考虑带约束

(1)先看一下保险合同的对象,示例代码如下:

/**
   * 保险合同的对象
   */
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);
    }
}

注意上例中的构造方法是default的访问权限,也就是不希望外部的对象直接通过new来构建保险合同对象;另外构造方法传入的是构建器对象,里面包含有所有保险合同需要的数据。

(2)看一下具体的构建器的实现,示例代码如下:

/**
  * 构造保险合同对象的构建器
  */
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的方法,来供外部设置需要的参数,为何说是类似于setter方法呢?请注意观察,每个这种方法都有返回值,返回的是构建器对象,这样客户端就可以通过连缀的方式来使用Builder,以创建他们需要的对象。

(3)接下来看看此时的Client,如何使用上面的构建器来创建保险合同对象,示例代码如下:

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

运行结果如下:

Now in Insurance Contract someOperation==001

看起来通过Builder模式构建对象也很简单,接下来,把约束加上去,看看如何实现。

  1. 使用Builder模式来构建复杂对象,考虑带约束规则

要带着约束规则构建复杂对象,大致的实现步骤与刚才的实现并没有什么不同,只是需要在刚才的实现上把约束规则添加上去。

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

一个是构建器的每一个类似于setter的方法,可以在这里进行单个数据的约束规则校验,如果不正确,就抛出IllegalStateException;

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

这里选择在构建器的build方法里面,进行数据的整体校验,由于其它的代码都没有变化,因此就不去赘述了,新的build方法的示例代码如下:

/**
 * 构建真正的对象并返回
 * @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);
}

你可以修改客户端的构建代码,传入不同的数据,看看这些约束规则是否能够正常工作,当然类似的规则还有很多,这里就不去深究了。

  1. 进一步,把构建器对象和被构建对象合并

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

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

还是看看示例会比较清楚,示例代码如下:

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

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

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. 松散耦合

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

  1. 可以很容易的改变产品的内部表示

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

  1. 更好的复用性

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

3.5 思考生成器模式##

  1. 生成器模式的本质

生成器模式的本质:分离整体构建算法和部件构造。

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

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

  1. 何时选用生成器模式

建议在如下情况中,选用生成器模式:

如果创建对象的算法,应该独立于该对象的组成部分以及它们的装配方式时;

如果同一个构建过程有着不同的表示时;

3.6 相关模式##

  1. 生成器模式和工厂方法模式

这两个模式可以组合使用。

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

  1. 生成器模式和抽象工厂模式

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

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

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

  1. 生成器模式和模板方法模式

这也是两个非常类似的模式。初看之下,不会觉得这两个模式有什么关联,但是仔细一思考,发现两个模式在功能上很类似。

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

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

当然两个模式也有很大的区别:

  1. 首先是模式的目的,生成器模式是用来构建复杂对象的,而模板方法是用来定义算法骨架,尤其是一些复杂的业务功能的处理算法的骨架;
  2. 其次是模式的实现,生成器模式是采用委托的方法,而模板方法是采用的继承的方式;
  3. 另外从使用的复杂度上,生成器模式需要组合Director和Builder对象,然后才能开始构建,要等构建完后才能获得最终的对象,而模板方法就没有这么麻烦,直接使用子类对象即可。
  1. 生成器模式和组合模式

这两个模式可以组合使用。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容