【白話設計模式十一】生成器模式(Builder)

#0 系列目錄#算法

#1 場景問題# ##1.1 繼續導出數據的應用框架## 在討論工廠方法模式的時候,提到了一個導出數據的應用框架。數據庫

對於導出數據的應用框架,一般在導出數據上,會有一些約定的方式,好比導出成:文本格式、數據庫備份形式、Excel格式、Xml格式等等。設計模式

在工廠方法模式章節裏面,討論並使用工廠方法模式來解決了如何選擇具體導出方式的問題,並無涉及到每種方式具體如何實現。換句話說,在討論工廠方法模式的時候,並無討論如何實現導出成文本、Xml等具體的格式,本章就來討論這個問題。安全

對於導出數據的應用框架,一般對於具體的導出內容和格式是有要求的,假如如今有以下的要求,簡單描述一下:app

導出的文件,無論什麼格式,都分紅三個部分,分別是文件頭、文件體和文件尾框架

在文件頭部分,須要描述以下信息:分公司或門市點編號、導出數據的日期,對於文本格式,中間用逗號分隔測試

在文件體部分,須要描述以下信息:表名稱、而後分條描述數據。對於文本格式,表名稱單獨佔一行,數據描述一行算一條數據,字段間用逗號分隔。ui

在文件尾部分,須要描述以下信息:輸出人this

如今就要來實現上述功能。爲了演示簡單點,在工廠方法模式裏面已經實現的功能,這裏就不去重複了,這裏只關心如何實現導出文件,並且只實現導出成文本格式和XML格式就能夠了,其它的就不去考慮了。.net

##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)從此可能會有不少不一樣輸出格式的要求,這就須要在處理過程不變的狀況下,能方便的切換不一樣的輸出格式的處理;

換句話來講,也就是構建每種格式的數據文件的處理過程,應該和具體的步驟實現分開,這樣就可以複用處理過程,並且能很容易的切換不一樣的輸出格式

但是該如何實現呢?

#2 解決方案# ##2.1 生成器模式來解決## 用來解決上述問題的一個合理的解決方案就是生成器模式。那麼什麼是生成器模式呢?

  1. 生成器模式定義

輸入圖片說明

  1. 應用生成器模式來解決的思路

仔細分析上面的實現,構建每種格式的數據文件的處理過程,這不就是構建過程嗎?而每種格式具體的步驟實現,不就至關因而不一樣的表示嗎?由於不一樣的步驟實現,決定了最終的表現也就不一樣。也就是說,上面的問題剛好就是生成器模式要解決的問題。

要實現一樣的構建過程能夠建立不一樣的表現,那麼一個天然的思路就是先把構建過程獨立出來,在生成器模式中把它稱爲指導者,由它來指導裝配過程,可是不負責每步具體的實現。固然,光有指導者是不夠的,必需要有能具體實現每步的對象,在生成器模式中稱這些實現對象爲生成器

這樣一來,指導者就是能夠重用的構建過程,而生成器是能夠被切換的具體實現。前面的實現中,每種具體的導出文件格式的實現就至關於生成器。

##2.2 模式結構和說明## 生成器模式的結構如圖8.1所示。

輸入圖片說明

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 使用生成器模式重寫示例## 要使用生成器模式來重寫示例,重要的任務就是要把指導者和生成器接口定義出來指導者就是用來執行那四個步驟的對象,而生成器是用來實現每種格式下,對於每一個步驟的具體實現的對象

按照生成器模式重寫示例的結構如圖8.2所示:

輸入圖片說明

  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. 生成器模式和抽象工廠模式

這兩個模式既類似又有區別,也能夠組合使用

先說類似性,這個在3.2小節的第一個小題目裏面已經詳細講述了,這裏就不去重複了。

再說說區別:抽象工廠模式的主要目的是建立產品簇,這個產品簇裏面的單個產品,就至關因而構成一個複雜對象的部件對象,抽象工廠對象建立完成事後就當即返回整個產品簇;而生成器模式的主要目的是按照構造算法,一步一步來構建一個複雜的產品對象,一般要等到整個構建過程結束事後,纔會獲得最終的產品對象。

事實上,這兩個模式是能夠組合使用的,在生成器模式的Builder實現中,須要建立各個部件對象,而這些部件對象是有關聯的,一般是構成一個複雜對象的部件對象,也就是說,Builder實現中,須要獲取構成一個複雜對象的產品簇,那天然就可使用抽象工廠模式來實現。這樣一來,由抽象工廠模式負責了部件對象的建立,Builder實現裏面就主要負責產品對象總體的構建了。

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

這也是兩個很是相似的模式。初看之下,不會以爲這兩個模式有什麼關聯,可是仔細一思考,發現兩個模式在功能上很相似。

模板方法模式主要是用來定義算法的骨架,把算法中某些步驟延遲到子類中實現。再想一想生成器模式,Director用來定義總體的構建算法,把算法中某些涉及到具體部件對象的建立和裝配的功能,委託給具體的Builder來實現

雖然生成器不是延遲到子類,是委託給Builder,但那只是具體實現方式上的差異,從實質上看兩個模式很相似,都是定義一個固定的算法骨架,而後把算法中的某些具體步驟交給其它類來完成,都能實現總體算法步驟和某些具體步驟實現的分離。

固然兩個模式也有很大的區別,首先是模式的目的,生成器模式是用來構建複雜對象的,而模板方法是用來定義算法骨架,尤爲是一些複雜的業務功能的處理算法的骨架;其次是模式的實現,生成器模式是採用委託的方法,而模板方法是採用的繼承的方式;另外從使用的複雜度上,生成器模式須要組合Director和Builder對象,而後才能開始構建,要等構建完後才能得到最終的對象,而模板方法就沒有這麼麻煩,直接使用子類對象便可。

  1. 生成器模式和組合模式

這兩個模式能夠組合使用。

對於複雜的組合結構,可使用生成器模式來一步一步構建。

相關文章
相關標籤/搜索