标签搜索

目 录CONTENT

文章目录

生成器(设计模式)

WP&CZH
2023-08-01 / 0 评论 / 0 点赞 / 264 阅读 / 4,535 字 / 正在检测是否收录...

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

对于导出数据的应用框架,通常在导出数据上,会有一些约定的方式,比如导出成:文本格式、数据库备份形式、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);
    }
}

运行结果如下:

img

运行结果

1.3 有何问题##

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

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

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

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

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

可是该如何实现呢?

2 解决方案#

2.1 生成器模式来解决##

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

  1. 生成器模式定义

img

生成器模式定义

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

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

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

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

2.2 模式结构和说明##

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

img

生成器模式的结构

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 使用生成器模式重写示例##

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

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

img

按照生成器模式重写示例的结构

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

看了上面的示例会发现,其实生成器模式也挺简单的,好好理解一下。通过上面的讲述,应该能很清晰的看出生成器模式的实现方式和它的优势所在了,那就是对同一个构建过程,只要配置不同的生成器实现,就会生成出不同表现的对象

0

评论区