1 場景問題#
1.1 繼續(xù)導(dǎo)出數(shù)據(jù)的應(yīng)用框架##
在討論工廠方法模式的時候,提到了一個導(dǎo)出數(shù)據(jù)的應(yīng)用框架。
對于導(dǎo)出數(shù)據(jù)的應(yīng)用框架,通常在導(dǎo)出數(shù)據(jù)上,會有一些約定的方式,比如導(dǎo)出成:文本格式、數(shù)據(jù)庫備份形式、Excel格式、Xml格式等等。
在工廠方法模式章節(jié)里面,討論并使用工廠方法模式來解決了如何選擇具體導(dǎo)出方式的問題,并沒有涉及到每種方式具體如何實現(xiàn)。換句話說,在討論工廠方法模式的時候,并沒有討論如何實現(xiàn)導(dǎo)出成文本、Xml等具體的格式,本章就來討論這個問題。
對于導(dǎo)出數(shù)據(jù)的應(yīng)用框架,通常對于具體的導(dǎo)出內(nèi)容和格式是有要求的,假如現(xiàn)在有如下的要求,簡單描述一下:
導(dǎo)出的文件,不管什么格式,都分成三個部分,分別是文件頭、文件體和文件尾
在文件頭部分,需要描述如下信息:分公司或門市點編號、導(dǎo)出數(shù)據(jù)的日期,對于文本格式,中間用逗號分隔
在文件體部分,需要描述如下信息:表名稱、然后分條描述數(shù)據(jù)。對于文本格式,表名稱單獨占一行,數(shù)據(jù)描述一行算一條數(shù)據(jù),字段間用逗號分隔。
在文件尾部分,需要描述如下信息:輸出人
現(xiàn)在就要來實現(xiàn)上述功能。為了演示簡單點,在工廠方法模式里面已經(jīng)實現(xiàn)的功能,這里就不去重復(fù)了,這里只關(guān)心如何實現(xiàn)導(dǎo)出文件,而且只實現(xiàn)導(dǎo)出成文本格式和XML格式就可以了,其它的就不去考慮了。
1.2 不用模式的解決方案##
不就是要實現(xiàn)導(dǎo)出數(shù)據(jù)到文本文件和XML文件嗎,其實不管什么格式,需要導(dǎo)出的數(shù)據(jù)是一樣的,只是具體導(dǎo)出到文件中的內(nèi)容,會隨著格式的不同而不同。
- 先來把描述文件各個部分的數(shù)據(jù)對象定義出來,先看描述輸出到文件頭的內(nèi)容的對象,示例代碼如下:
/**
* 描述輸出到文件頭的內(nèi)容的對象
*/
public class ExportHeaderModel {
/**
* 分公司或門市點編號
*/
private String depId;
/**
* 導(dǎo)出數(shù)據(jù)的日期
*/
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;
}
}
接下來看看描述輸出數(shù)據(jù)的對象,示例代碼如下:
/**
* 描述輸出數(shù)據(jù)的對象
*/
public class ExportDataModel {
/**
* 產(chǎn)品編號
*/
private String productId;
/**
* 銷售價格
*/
private double price;
/**
* 銷售數(shù)量
*/
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;
}
}
接下來看看描述輸出到文件尾的內(nèi)容的對象,示例代碼如下:
/**
* 描述輸出到文件尾的內(nèi)容的對象
*/
public class ExportFooterModel {
/**
* 輸出人
*/
private String exportUser;
public String getExportUser() {
return exportUser;
}
public void setExportUser(String exportUser) {
this.exportUser = exportUser;
}
}
- 接下來具體的看看導(dǎo)出的實現(xiàn),先看導(dǎo)出數(shù)據(jù)到文本文件的對象,主要就是要實現(xiàn)拼接輸出的內(nèi)容,示例代碼如下:
/**
* 導(dǎo)出數(shù)據(jù)到文本文件的對象
*/
public class ExportToTxt {
/**
* 導(dǎo)出數(shù)據(jù)到文本文件
* @param ehm 文件頭的內(nèi)容
* @param mapData 數(shù)據(jù)的內(nèi)容
* @param efm 文件尾的內(nèi)容
*/
public void export(ExportHeaderModel ehm,Map<String,Collection<ExportDataModel>> mapData,ExportFooterModel efm){
//用來記錄最終輸出的文件內(nèi)容
StringBuffer buffer = new StringBuffer();
//1:先來拼接文件頭的內(nèi)容
buffer.append(ehm.getDepId()+","+ehm.getExportDate()+"\n");
//2:接著來拼接文件體的內(nèi)容
for(String tblName : mapData.keySet()){
//先拼接表名稱
buffer.append(tblName+"\n");
//然后循環(huán)拼接具體數(shù)據(jù)
for(ExportDataModel edm : mapData.get(tblName)){
buffer.append(edm.getProductId()+","+edm.getPrice()+","+edm.getAmount()+"\n");
}
}
//3:接著來拼接文件尾的內(nèi)容
buffer.append(efm.getExportUser());
//為了演示簡潔性,這里就不去寫輸出文件的代碼了
//把要輸出的內(nèi)容輸出到控制臺看看
System.out.println("輸出到文本文件的內(nèi)容:\n"+buffer);
}
}
- 接下來看看導(dǎo)出數(shù)據(jù)到XML文件的對象,比較麻煩,要按照XML的格式進(jìn)行拼接,示例代碼如下:
/**
* 導(dǎo)出數(shù)據(jù)到XML文件的對象
*/
public class ExportToXml {
/**
* 導(dǎo)出數(shù)據(jù)到XML文件
* @param ehm 文件頭的內(nèi)容
* @param mapData 數(shù)據(jù)的內(nèi)容
* @param efm 文件尾的內(nèi)容
*/
public void export(ExportHeaderModel ehm,Map<String,Collection<ExportDataModel>> mapData,ExportFooterModel efm){
//用來記錄最終輸出的文件內(nèi)容
StringBuffer buffer = new StringBuffer();
//1:先來拼接文件頭的內(nèi)容
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:接著來拼接文件體的內(nèi)容
buffer.append(" <Body>\n");
for(String tblName : mapData.keySet()){
//先拼接表名稱
buffer.append(" <Datas TableName=\""+tblName+"\">\n");
//然后循環(huán)拼接具體數(shù)據(jù)
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:接著來拼接文件尾的內(nèi)容
buffer.append(" <Footer>\n");
buffer.append(" <ExportUser>"+efm.getExportUser()+"</ExportUser>\n");
buffer.append(" </Footer>\n");
buffer.append("</Report>\n");
//為了演示簡潔性,這里就不去寫輸出文件的代碼了
//把要輸出的內(nèi)容輸出到控制臺看看
System.out.println("輸出到XML文件的內(nèi)容:\n"+buffer);
}
}
- 看看客戶端,如何來使用這些對象,示例代碼如下:
public class Client {
public static void main(String[] args) {
//準(zhǔn)備測試數(shù)據(jù)
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("產(chǎn)品001號");
edm1.setPrice(100);
edm1.setAmount(80);
ExportDataModel edm2 = new ExportDataModel();
edm2.setProductId("產(chǎn)品002號");
edm2.setPrice(99);
edm2.setAmount(55);
//把數(shù)據(jù)組裝起來
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);
}
}
運行結(jié)果如下:
1.3 有何問題##
仔細(xì)觀察上面的實現(xiàn),會發(fā)現(xiàn),不管是輸出成文本文件,還是輸出到XML文件,在實現(xiàn)的時候,步驟基本上都是一樣的,都大致分成了如下四步:
- 先拼接文件頭的內(nèi)容;
- 然后拼接文件體的內(nèi)容;
- 再拼接文件尾的內(nèi)容;
- 最后把拼接好的內(nèi)容輸出出去成為文件;
也就是說,對于不同的輸出格式,處理步驟是一樣的,但是具體每步的實現(xiàn)是不一樣的。按照現(xiàn)在的實現(xiàn)方式,就存在如下的問題:
構(gòu)建每種輸出格式的文件內(nèi)容的時候,都會重復(fù)這幾個處理步驟,應(yīng)該提煉出來,形成公共的處理過程;
今后可能會有很多不同輸出格式的要求,這就需要在處理過程不變的情況下,能方便的切換不同的輸出格式的處理;
換句話來說,也就是構(gòu)建每種格式的數(shù)據(jù)文件的處理過程,應(yīng)該和具體的步驟實現(xiàn)分開,這樣就能夠復(fù)用處理過程,而且能很容易的切換不同的輸出格式。
可是該如何實現(xiàn)呢?
2 解決方案#
2.1 生成器模式來解決##
用來解決上述問題的一個合理的解決方案就是生成器模式。那么什么是生成器模式呢?
- 生成器模式定義
- 應(yīng)用生成器模式來解決的思路
仔細(xì)分析上面的實現(xiàn),構(gòu)建每種格式的數(shù)據(jù)文件的處理過程,這不就是構(gòu)建過程嗎?而每種格式具體的步驟實現(xiàn),不就相當(dāng)于是不同的表示嗎?因為不同的步驟實現(xiàn),決定了最終的表現(xiàn)也就不同。也就是說,上面的問題恰好就是生成器模式要解決的問題。
要實現(xiàn)同樣的構(gòu)建過程可以創(chuàng)建不同的表現(xiàn),那么一個自然的思路就是先把構(gòu)建過程獨立出來,在生成器模式中把它稱為指導(dǎo)者,由它來指導(dǎo)裝配過程,但是不負(fù)責(zé)每步具體的實現(xiàn)。當(dāng)然,光有指導(dǎo)者是不夠的,必須要有能具體實現(xiàn)每步的對象,在生成器模式中稱這些實現(xiàn)對象為生成器。
這樣一來,指導(dǎo)者就是可以重用的構(gòu)建過程,而生成器是可以被切換的具體實現(xiàn)。前面的實現(xiàn)中,每種具體的導(dǎo)出文件格式的實現(xiàn)就相當(dāng)于生成器。
2.2 模式結(jié)構(gòu)和說明##
生成器模式的結(jié)構(gòu)如圖所示:
Builder:生成器接口,定義創(chuàng)建一個Product對象所需的各個部件的操作。
ConcreteBuilder:具體的生成器實現(xiàn),實現(xiàn)各個部件的創(chuàng)建,并負(fù)責(zé)組裝Product對象的各個部件,同時還提供一個讓用戶獲取組裝完成后的產(chǎn)品對象的方法。
Director:指導(dǎo)者,也被稱為導(dǎo)向者,主要用來使用Builder接口,以一個統(tǒng)一的過程來構(gòu)建所需要的Product對象。
Product:產(chǎn)品,表示被生成器構(gòu)建的復(fù)雜對象,包含多個部件。
2.3 生成器模式示例代碼##
- 先看看生成器的接口定義,示例代碼如下:
/**
* 生成器接口,定義創(chuàng)建一個產(chǎn)品對象所需的各個部件的操作
*/
public interface Builder {
/**
* 示意方法,構(gòu)建某個部件
*/
public void buildPart();
}
- 再看看具體的生成器實現(xiàn),示例代碼如下:
/**
* 具體的生成器實現(xiàn)對象
*/
public class ConcreteBuilder implements Builder {
/**
* 生成器最終構(gòu)建的產(chǎn)品對象
*/
private Product resultProduct;
/**
* 獲取生成器最終構(gòu)建的產(chǎn)品對象
* @return 生成器最終構(gòu)建的產(chǎn)品對象
*/
public Product getResult() {
return resultProduct;
}
public void buildPart() {
//構(gòu)建某個部件的功能處理
}
}
- 看看相應(yīng)的產(chǎn)品對象的接口示意,示例代碼如下:
/**
* 被構(gòu)建的產(chǎn)品對象的接口
*/
public interface Product {
//定義產(chǎn)品的操作
}
- 再來看看指導(dǎo)者的實現(xiàn)示意,示例代碼如下:
/**
* 指導(dǎo)者,指導(dǎo)使用生成器的接口來構(gòu)建產(chǎn)品的對象
*/
public class Director {
/**
* 持有當(dāng)前需要使用的生成器對象
*/
private Builder builder;
/**
* 構(gòu)造方法,傳入生成器對象
* @param builder 生成器對象
*/
public Director(Builder builder) {
this.builder = builder;
}
/**
* 示意方法,指導(dǎo)生成器構(gòu)建最終的產(chǎn)品對象
*/
public void construct() {
//通過使用生成器接口來構(gòu)建最終的產(chǎn)品對象
builder.buildPart();
}
}
2.4 使用生成器模式重寫示例##
要使用生成器模式來重寫示例,重要的任務(wù)就是要把指導(dǎo)者和生成器接口定義出來。指導(dǎo)者就是用來執(zhí)行那四個步驟的對象,而生成器是用來實現(xiàn)每種格式下,對于每個步驟的具體實現(xiàn)的對象。
按照生成器模式重寫示例的結(jié)構(gòu)如圖所示:
前面示例中的三個數(shù)據(jù)模型對象還繼續(xù)沿用,這里就不去贅述了。
先來看看定義的Builder接口,主要是把導(dǎo)出各種格式文件的處理過程的步驟定義出來,每個步驟負(fù)責(zé)構(gòu)建最終導(dǎo)出文件的一部分。示例代碼如下:
/**
* 生成器接口,定義創(chuàng)建一個輸出文件對象所需的各個部件的操作
*/
public interface Builder {
/**
* 構(gòu)建輸出文件的Header部分
* @param ehm 文件頭的內(nèi)容
*/
public void buildHeader(ExportHeaderModel ehm);
/**
* 構(gòu)建輸出文件的Body部分
* @param mapData 要輸出的數(shù)據(jù)的內(nèi)容
*/
public void buildBody(Map<String,Collection<ExportDataModel>> mapData);
/**
* 構(gòu)建輸出文件的Footer部分
* @param efm 文件尾的內(nèi)容
*/
public void buildFooter(ExportFooterModel efm);
}
- 接下來看看具體的生成器實現(xiàn),其實就是把原來示例中,寫在一起的實現(xiàn),分拆成多個步驟實現(xiàn)了,先看看導(dǎo)出數(shù)據(jù)到文本文件的生成器實現(xiàn),示例代碼如下:
/**
* 實現(xiàn)導(dǎo)出數(shù)據(jù)到文本文件的的生成器對象
*/
public class TxtBuilder implements Builder {
/**
* 用來記錄構(gòu)建的文件的內(nèi)容,相當(dāng)于產(chǎn)品
*/
private StringBuffer buffer = new StringBuffer();
public void buildBody(Map<String, Collection<ExportDataModel>> mapData) {
for(String tblName : mapData.keySet()){
//先拼接表名稱
buffer.append(tblName+"\n");
//然后循環(huán)拼接具體數(shù)據(jù)
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;
}
}
再看看導(dǎo)出數(shù)據(jù)到XML文件的生成器實現(xiàn),示例代碼如下:
/**
* 實現(xiàn)導(dǎo)出數(shù)據(jù)到XML文件的的生成器對象
*/
public class XmlBuilder implements Builder {
/**
* 用來記錄構(gòu)建的文件的內(nèi)容,相當(dāng)于產(chǎn)品
*/
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");
//然后循環(huán)拼接具體數(shù)據(jù)
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;
}
}
- 指導(dǎo)者
有了具體的生成器實現(xiàn)后,需要有指導(dǎo)者來指導(dǎo)它進(jìn)行具體的產(chǎn)品構(gòu)建,由于構(gòu)建的產(chǎn)品是文本內(nèi)容,所以就不用單獨定義產(chǎn)品對象了。示例代碼如下:
/**
* 指導(dǎo)者,指導(dǎo)使用生成器的接口來構(gòu)建輸出的文件的對象
*/
public class Director {
/**
* 持有當(dāng)前需要使用的生成器對象
*/
private Builder builder;
/**
* 構(gòu)造方法,傳入生成器對象
* @param builder 生成器對象
*/
public Director(Builder builder) {
this.builder = builder;
}
/**
* 指導(dǎo)生成器構(gòu)建最終的輸出的文件的對象
* @param ehm 文件頭的內(nèi)容
* @param mapData 數(shù)據(jù)的內(nèi)容
* @param efm 文件尾的內(nèi)容
*/
public void construct(ExportHeaderModel ehm,Map<String,Collection<ExportDataModel>> mapData,ExportFooterModel efm) {
//1:先構(gòu)建Header
builder.buildHeader(ehm);
//2:然后構(gòu)建Body
builder.buildBody(mapData);
//3:然后構(gòu)建Footer
builder.buildFooter(efm);
}
}
- 都實現(xiàn)得差不多了,該來寫個客戶端好好測試一下了。示例代碼如下:
public class Client {
public static void main(String[] args) {
//準(zhǔn)備測試數(shù)據(jù)
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("產(chǎn)品001號");
edm1.setPrice(100);
edm1.setAmount(80);
ExportDataModel edm2 = new ExportDataModel();
edm2.setProductId("產(chǎn)品002號");
edm2.setPrice(99);
edm2.setAmount(55);
//把數(shù)據(jù)組裝起來
col.add(edm1);
col.add(edm2);
mapData.put("銷售記錄表", col);
ExportFooterModel efm = new ExportFooterModel();
efm.setExportUser("張三");
//測試輸出到文本文件
TxtBuilder txtBuilder = new TxtBuilder();
//創(chuàng)建指導(dǎo)者對象
Director director = new Director(txtBuilder);
director.construct(ehm, mapData, efm);
//把要輸出的內(nèi)容輸出到控制臺看看
System.out.println("輸出到文本文件的內(nèi)容:\n"+txtBuilder.getResult());
//測試輸出到xml文件
XmlBuilder xmlBuilder = new XmlBuilder();
Director director2 = new Director(xmlBuilder);
director2.construct(ehm, mapData, efm);
//把要輸出的內(nèi)容輸出到控制臺看看
System.out.println("輸出到XML文件的內(nèi)容:\n"+xmlBuilder.getResult());
}
}
看了上面的示例會發(fā)現(xiàn),其實生成器模式也挺簡單的,好好理解一下。通過上面的講述,應(yīng)該能很清晰的看出生成器模式的實現(xiàn)方式和它的優(yōu)勢所在了,那就是對同一個構(gòu)建過程,只要配置不同的生成器實現(xiàn),就會生成出不同表現(xiàn)的對象。
3 模式講解#
3.1 認(rèn)識生成器模式##
- 生成器模式的功能
生成器模式的主要功能是構(gòu)建復(fù)雜的產(chǎn)品,而且是細(xì)化的,分步驟的構(gòu)建產(chǎn)品,也就是生成器模式重在解決一步一步構(gòu)造復(fù)雜對象的問題。如果光是這么認(rèn)識生成器模式的功能是不夠的。
更為重要的是,這個構(gòu)建的過程是統(tǒng)一的,固定不變的,變化的部分放到生成器部分了,只要配置不同的生成器,那么同樣的構(gòu)建過程,就能構(gòu)建出不同的產(chǎn)品表示來。
再直白點說,生成器模式的重心在于分離構(gòu)建算法和具體的構(gòu)造實現(xiàn),從而使得構(gòu)建算法可以重用,具體的構(gòu)造實現(xiàn)可以很方便的擴展和切換,從而可以靈活的組合來構(gòu)造出不同的產(chǎn)品對象。
- 生成器模式的構(gòu)成
要特別注意,生成器模式分成兩個很重要的部分:
一個部分是Builder接口這邊,這邊是定義了如何構(gòu)建各個部件,也就是知道每個部件功能如何實現(xiàn),以及如何裝配這些部件到產(chǎn)品中去。
另外一個部分是Director這邊,Director是知道如何組合來構(gòu)建產(chǎn)品,也就是說Director負(fù)責(zé)整體的構(gòu)建算法,而且通常是分步驟的來執(zhí)行。
不管如何變化,Builder模式都存在這么兩個部分,一個部分是部件構(gòu)造和產(chǎn)品裝配,另一個部分是整體構(gòu)建的算法。認(rèn)識這點是很重要的,因為在生成器模式中,強調(diào)的是固定整體構(gòu)建的算法,而靈活擴展和切換部件的具體構(gòu)造和產(chǎn)品裝配的方式,所以要嚴(yán)格區(qū)分這兩個部分。
在Director實現(xiàn)整體構(gòu)建算法的時候,遇到需要創(chuàng)建和組合具體部件的時候,就會把這些功能通過委托,交給Builder去完成。
- 生成器模式的使用
應(yīng)用生成器模式的時候,可以讓客戶端創(chuàng)造Director,在Director里面封裝整體構(gòu)建算法,然后讓Director去調(diào)用Builder,讓Builder來封裝具體部件的構(gòu)建功能,這就跟前面的例子一樣。
還有一種退化的情況,就是讓客戶端和Director融合起來,讓客戶端直接去操作Builder,就好像是指導(dǎo)者自己想要給自己構(gòu)建產(chǎn)品一樣。
- 生成器模式的調(diào)用順序示意圖
3.2 生成器模式的實現(xiàn)##
- 生成器的實現(xiàn)
實際上在Builder接口的實現(xiàn)中,每個部件構(gòu)建的方法里面,除了部件裝配外,也可以實現(xiàn)如何具體的創(chuàng)建各個部件對象,也就是說每個方法都可以有兩部分功能,一個是創(chuàng)建部件對象,一個是組裝部件。
在構(gòu)建部件的方法里面可以實現(xiàn)選擇并創(chuàng)建具體的部件對象,然后再把這個部件對象組裝到產(chǎn)品對象中去,這樣一來,Builder就可以和工廠方法配合使用了。
再進(jìn)一步,如果在實現(xiàn)Builder的時候,只有創(chuàng)建對象的功能,而沒有組裝的功能,那么這個時候的Builder實現(xiàn)跟抽象工廠的實現(xiàn)是類似的。
這種情況下,Builder接口就類似于抽象工廠的接口,Builder的具體實現(xiàn)就類似于具體的工廠,而且Builder接口里面定義的創(chuàng)建各個部件的方法也是有關(guān)聯(lián)的,這些方法是構(gòu)建一個復(fù)雜對象所需要的部件對象,仔細(xì)想想,是不是非常類似呢。
- 指導(dǎo)者的實現(xiàn)
在生成器模式里面,指導(dǎo)者承擔(dān)的是整體構(gòu)建算法部分,是相對不變的部分。因此在實現(xiàn)指導(dǎo)者的時候,把變化的部分分離出去是很重要的。
其實指導(dǎo)者分離出去的變化部分,就到了生成器那邊,指導(dǎo)者知道整體的構(gòu)建算法,就是不知道如何具體的創(chuàng)建和裝配部件對象。
因此真正的指導(dǎo)者實現(xiàn),并不僅僅是如同前面示例那樣,簡單的按照一定順序調(diào)用生成器的方法來生成對象,并沒有這么簡單。應(yīng)該是有較為復(fù)雜的算法和運算過程,在運算過程中根據(jù)需要,才會調(diào)用生成器的方法來生成部件對象。
- 指導(dǎo)者和生成器的交互
在生成器模式里面,指導(dǎo)者和生成器的交互,是通過生成器的那些buildPart方法來完成的。在前面的示例中,指導(dǎo)者和生成器是沒有太多相互交互的,指導(dǎo)者僅僅只是簡單的調(diào)用了一下生成器的方法,在實際開發(fā)中,這是遠(yuǎn)遠(yuǎn)不夠的。
指導(dǎo)者通常會實現(xiàn)比較復(fù)雜的算法或者是運算過程,在實際中很可能會有這樣的情況:
在運行指導(dǎo)者的時候,會按照整體構(gòu)建算法的步驟進(jìn)行運算,可能先運行前幾步運算,到了某一步驟,需要具體創(chuàng)建某個部件對象了,然后就調(diào)用Builder中創(chuàng)建相應(yīng)部件的方法來創(chuàng)建具體的部件。同時,把前面運算得到的數(shù)據(jù)傳遞給Builder,因為在Builder內(nèi)部實現(xiàn)創(chuàng)建和組裝部件的時候,可能會需要這些數(shù)據(jù);
Builder創(chuàng)建完具體的部件對象后,會把創(chuàng)建好的部件對象返回給指導(dǎo)者,指導(dǎo)者繼續(xù)后續(xù)的算法運算,可能會用到已經(jīng)創(chuàng)建好的對象;
如此反復(fù)下去,直到整個構(gòu)建算法運行完成,那么最終的產(chǎn)品對象也就創(chuàng)建好了;
通過上面的描述,可以看出指導(dǎo)者和生成器是需要交互的,方式就是通過生成器方法的參數(shù)和返回值,來回的傳遞數(shù)據(jù)。事實上,指導(dǎo)者是通過委托的方式來把功能交給生成器去完成。
- 返回裝配好的產(chǎn)品的方法
在標(biāo)準(zhǔn)的生成器模式里面,在Builder實現(xiàn)里面會提供一個返回裝配好的產(chǎn)品的方法,在Builder接口上是沒有的。它考慮的是最終的對象一定要通過部件構(gòu)建和裝配,才算真正創(chuàng)建了,而具體干活的就是這個Builder實現(xiàn),雖然指導(dǎo)者也參與了,但是指導(dǎo)者是不負(fù)責(zé)具體的部件創(chuàng)建和組裝的,因此客戶端是從Builder實現(xiàn)里面獲取最終裝配好的產(chǎn)品。
當(dāng)然在Java里面,我們也可以把這個方法添加到Builder接口里面。
- 關(guān)于被構(gòu)建的產(chǎn)品的接口
在使用生成器模式的時候,大多數(shù)情況下是不知道最終構(gòu)建出來的產(chǎn)品是什么樣的,所以在標(biāo)準(zhǔn)的生成器模式里面,一般是不需要對產(chǎn)品定義抽象接口的,因為最終構(gòu)造的產(chǎn)品千差萬別,給這些產(chǎn)品定義公共接口幾乎是沒有意義的。
3.3 使用生成器模式構(gòu)建復(fù)雜對象##
考慮這樣一個實際應(yīng)用,要創(chuàng)建一個保險合同的對象,里面很多屬性的值都有約束,要求創(chuàng)建出來的對象是滿足這些約束規(guī)則的。約束規(guī)則比如:保險合同通常情況下可以和個人簽訂,也可以和某個公司簽訂,但是一份保險合同不能同時與個人和公司簽訂。這個對象里面有很多類似這樣的約束,那么該如何來創(chuàng)建這個對象呢?
要想簡潔直觀、安全性好、又具有很好的擴展性的來創(chuàng)建這個對象的話,一個很好的選擇就是使用Builder模式,把復(fù)雜的創(chuàng)建過程通過buidler來實現(xiàn)。
采用Builder模式來構(gòu)建復(fù)雜的對象,通常會對Builder模式進(jìn)行一定的簡化,因為目標(biāo)明確,就是創(chuàng)建某個復(fù)雜對象,因此做適當(dāng)簡化會使程序更簡潔,大致簡化如下:
由于是用Builder模式來創(chuàng)建某個對象,因此就沒有必要再定義一個Builder接口,直接提供一個具體的構(gòu)建器類就可以了;
對于創(chuàng)建一個復(fù)雜的對象,可能會有很多種不同的選擇和步驟,干脆去掉“指導(dǎo)者”,把指導(dǎo)者的功能和Client的功能合并起來,也就是說,Client這個時候就相當(dāng)于指導(dǎo)者,它來指導(dǎo)構(gòu)建器類去構(gòu)建需要的復(fù)雜對象;
還是來看看示例會比較清楚,為了實例簡單,先不去考慮約束的實現(xiàn),只是考慮如何通過Builder模式來構(gòu)建復(fù)雜對象。
- 使用Builder模式來構(gòu)建復(fù)雜對象,先不考慮帶約束
(1)先看一下保險合同的對象,示例代碼如下:
/**
* 保險合同的對象
*/
public class InsuranceContract {
/**
* 保險合同編號
*/
private String contractId;
/**
* 被保險人員的名稱,同一份保險合同,要么跟人員簽訂,要么跟公司簽訂,
* 也就是說,"被保險人員"和"被保險公司"這兩個屬性,不可能同時有值
*/
private String personName;
/**
* 被保險公司的名稱
*/
private String companyName;
/**
* 保險開始生效的日期
*/
private long beginDate;
/**
* 保險失效的日期,一定會大于保險開始生效的日期
*/
private long endDate;
/**
* 示例:其它數(shù)據(jù)
*/
private String otherData;
/**
* 構(gòu)造方法,訪問級別是同包能訪問
*/
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);
}
}
注意上例中的構(gòu)造方法是default的訪問權(quán)限,也就是不希望外部的對象直接通過new來構(gòu)建保險合同對象;另外構(gòu)造方法傳入的是構(gòu)建器對象,里面包含有所有保險合同需要的數(shù)據(jù)。
(2)看一下具體的構(gòu)建器的實現(xiàn),示例代碼如下:
/**
* 構(gòu)造保險合同對象的構(gòu)建器
*/
public class ConcreteBuilder {
private String contractId;
private String personName;
private String companyName;
private long beginDate;
private long endDate;
private String otherData;
/**
* 構(gòu)造方法,傳入必須要有的參數(shù)
* @param contractId 保險合同編號
* @param beginDate 保險開始生效的日期
* @param endDate 保險失效的日期
*/
public ConcreteBuilder(String contractId,long beginDate,long endDate){
this.contractId = contractId;
this.beginDate = beginDate;
this.endDate = endDate;
}
/**
* 選填數(shù)據(jù),被保險人員的名稱
* @param personName 被保險人員的名稱
* @return 構(gòu)建器對象
*/
public ConcreteBuilder setPersonName(String personName){
this.personName = personName;
return this;
}
/**
* 選填數(shù)據(jù),被保險公司的名稱
* @param companyName 被保險公司的名稱
* @return 構(gòu)建器對象
*/
public ConcreteBuilder setCompanyName(String companyName){
this.companyName = companyName;
return this;
}
/**
* 選填數(shù)據(jù),其它數(shù)據(jù)
* @param otherData 其它數(shù)據(jù)
* @return 構(gòu)建器對象
*/
public ConcreteBuilder setOtherData(String otherData){
this.otherData = otherData;
return this;
}
/**
* 構(gòu)建真正的對象并返回
* @return 構(gòu)建的保險合同的對象
*/
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;
}
}
注意上例中,構(gòu)建器提供了類似于setter的方法,來供外部設(shè)置需要的參數(shù),為何說是類似于setter方法呢?請注意觀察,每個這種方法都有返回值,返回的是構(gòu)建器對象,這樣客戶端就可以通過連綴的方式來使用Builder,以創(chuàng)建他們需要的對象。
(3)接下來看看此時的Client,如何使用上面的構(gòu)建器來創(chuàng)建保險合同對象,示例代碼如下:
public class Client {
public static void main(String[] args) {
//創(chuàng)建構(gòu)建器
ConcreteBuilder builder = new ConcreteBuilder("001",12345L,67890L);
//設(shè)置需要的數(shù)據(jù),然后構(gòu)建保險合同對象
InsuranceContract contract = builder.setPersonName("張三").setOtherData("test").build();
//操作保險合同對象的方法
contract.someOperation();
}
}
運行結(jié)果如下:
Now in Insurance Contract someOperation==001
看起來通過Builder模式構(gòu)建對象也很簡單,接下來,把約束加上去,看看如何實現(xiàn)。
- 使用Builder模式來構(gòu)建復(fù)雜對象,考慮帶約束規(guī)則
要帶著約束規(guī)則構(gòu)建復(fù)雜對象,大致的實現(xiàn)步驟與剛才的實現(xiàn)并沒有什么不同,只是需要在剛才的實現(xiàn)上把約束規(guī)則添加上去。
通常有兩個地方可以添加約束規(guī)則:
一個是構(gòu)建器的每一個類似于setter的方法,可以在這里進(jìn)行單個數(shù)據(jù)的約束規(guī)則校驗,如果不正確,就拋出IllegalStateException;
另一個是構(gòu)建器的build方法,在創(chuàng)建保險合同對象之前,對所有的數(shù)據(jù)都可以進(jìn)行數(shù)據(jù)的約束規(guī)則校驗,尤其是那些涉及到幾個數(shù)據(jù)之間的約束關(guān)系,在這里校驗會比較合適。如果不正確,同樣拋出IllegalStateException;
這里選擇在構(gòu)建器的build方法里面,進(jìn)行數(shù)據(jù)的整體校驗,由于其它的代碼都沒有變化,因此就不去贅述了,新的build方法的示例代碼如下:
/**
* 構(gòu)建真正的對象并返回
* @return 構(gòu)建的保險合同的對象
*/
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);
}
你可以修改客戶端的構(gòu)建代碼,傳入不同的數(shù)據(jù),看看這些約束規(guī)則是否能夠正常工作,當(dāng)然類似的規(guī)則還有很多,這里就不去深究了。
- 進(jìn)一步,把構(gòu)建器對象和被構(gòu)建對象合并
其實,在實際開發(fā)中,如果構(gòu)建器對象和被構(gòu)建的對象是這樣分開的話,可能會導(dǎo)致同包內(nèi)的對象不使用構(gòu)建器來構(gòu)建對象,而是直接去使用new來構(gòu)建對象,這會導(dǎo)致錯誤;另外,這個構(gòu)建器的功能就是為了創(chuàng)建被構(gòu)建的對象,完全可以不用單獨一個類。
對于這種情況,重構(gòu)的手法通常是將類內(nèi)聯(lián)化(Inline Class),放到這里來,簡單點說就是把構(gòu)建器對象合并到被構(gòu)建對象里面去。
還是看看示例會比較清楚,示例代碼如下:
public class InsuranceContract {
private String contractId;
private String personName;
private String companyName;
private long beginDate;
private long endDate;
private String otherData;
/**
* 構(gòu)造方法,訪問級別是私有的
*/
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;
}
/**
* 構(gòu)造保險合同對象的構(gòu)建器,作為保險合同的類級內(nèi)部類
*/
public static class ConcreteBuilder {
private String contractId;
private String personName;
private String companyName;
private long beginDate;
private long endDate;
private String otherData;
/**
* 構(gòu)造方法,傳入必須要有的參數(shù)
* @param contractId 保險合同編號
* @param beginDate 保險開始生效的日期
* @param endDate 保險失效的日期
*/
public ConcreteBuilder(String contractId,long beginDate,long endDate){
this.contractId = contractId;
this.beginDate = beginDate;
this.endDate = endDate;
}
/**
* 選填數(shù)據(jù),被保險人員的名稱
* @param personName 被保險人員的名稱
* @return 構(gòu)建器對象
*/
public ConcreteBuilder setPersonName(String personName){
this.personName = personName;
return this;
}
/**
* 選填數(shù)據(jù),被保險公司的名稱
* @param companyName 被保險公司的名稱
* @return 構(gòu)建器對象
*/
public ConcreteBuilder setCompanyName(String companyName){
this.companyName = companyName;
return this;
}
/**
* 選填數(shù)據(jù),其它數(shù)據(jù)
* @param otherData 其它數(shù)據(jù)
* @return 構(gòu)建器對象
*/
public ConcreteBuilder setOtherData(String otherData){
this.otherData = otherData;
return this;
}
/**
* 構(gòu)建真正的對象并返回
* @return 構(gòu)建的保險合同的對象
*/
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);
}
}
通過上面的示例可以看出,這種實現(xiàn)方式會更簡單和直觀。此時客戶端的寫法也發(fā)生了一點變化,主要就是創(chuàng)建構(gòu)造器的地方需要變化,示例代碼如下:
public class Client {
public static void main(String[] args) {
//創(chuàng)建構(gòu)建器
InsuranceContract.ConcreteBuilder builder = new InsuranceContract.ConcreteBuilder("001",12345L,67890L);
//設(shè)置需要的數(shù)據(jù),然后構(gòu)建保險合同對象
InsuranceContract contract = builder.setPersonName("張三").setOtherData("test").build();
//操作保險合同對象的方法
contract.someOperation();
}
}
3.4 生成器模式的優(yōu)缺點##
- 松散耦合
生成器模式可以用同一個構(gòu)建算法,構(gòu)建出表現(xiàn)上完全不同的產(chǎn)品,實現(xiàn)產(chǎn)品構(gòu)建和產(chǎn)品表現(xiàn)上的分離。生成器模式正是把產(chǎn)品構(gòu)建的過程獨立出來,使它和具體產(chǎn)品的表現(xiàn)松散耦合,從而使得構(gòu)建算法可以復(fù)用,而具體產(chǎn)品表現(xiàn)也可以靈活的、方便的擴展和切換。
- 可以很容易的改變產(chǎn)品的內(nèi)部表示
在生成器模式中,由于Builder對象只是提供接口給Director使用,那么具體的部件創(chuàng)建和裝配方式是被Builder接口隱藏了的,Director并不知道這些具體的實現(xiàn)細(xì)節(jié)。這樣一來,要想改變產(chǎn)品的內(nèi)部表示,只需要切換Builder的具體實現(xiàn)即可,不用管Director,因此變得很容易。
- 更好的復(fù)用性
生成器模式很好的實現(xiàn)了構(gòu)建算法和具體產(chǎn)品實現(xiàn)的分離,這樣一來,使得構(gòu)建產(chǎn)品的算法可以復(fù)用。同樣的道理,具體產(chǎn)品的實現(xiàn)也可以復(fù)用,同一個產(chǎn)品的實現(xiàn),可以配合不同的構(gòu)建算法使用。
3.5 思考生成器模式##
- 生成器模式的本質(zhì)
生成器模式的本質(zhì):分離整體構(gòu)建算法和部件構(gòu)造。
構(gòu)建一個復(fù)雜的對象,本來就有構(gòu)建的過程,以及構(gòu)建過程中具體的實現(xiàn),生成器模式就是用來分離這兩個部分,從而使得程序結(jié)構(gòu)更松散、擴展更容易、復(fù)用性更好,同時也會使得代碼更清晰,意圖更明確。
雖然在生成器模式的整體構(gòu)建算法中,會一步一步引導(dǎo)Builder來構(gòu)建對象,但這并不是說生成器就主要是用來實現(xiàn)分步驟構(gòu)建對象的。生成器模式的重心還是在于分離整體構(gòu)建算法和部件構(gòu)造,而分步驟構(gòu)建對象不過是整體構(gòu)建算法的一個簡單表現(xiàn),或者說是一個附帶產(chǎn)物。
- 何時選用生成器模式
建議在如下情況中,選用生成器模式:
如果創(chuàng)建對象的算法,應(yīng)該獨立于該對象的組成部分以及它們的裝配方式時;
如果同一個構(gòu)建過程有著不同的表示時;
3.6 相關(guān)模式##
- 生成器模式和工廠方法模式
這兩個模式可以組合使用。
生成器模式的Builder實現(xiàn)中,通常需要選擇具體的部件實現(xiàn),一個可行的方案就是實現(xiàn)成為工廠方法,通過工廠方法來獲取具體的部件對象,然后再進(jìn)行部件的裝配。
- 生成器模式和抽象工廠模式
這兩個模式既相似又有區(qū)別,也可以組合使用
說說區(qū)別:抽象工廠模式的主要目的是創(chuàng)建產(chǎn)品簇,這個產(chǎn)品簇里面的單個產(chǎn)品,就相當(dāng)于是構(gòu)成一個復(fù)雜對象的部件對象,抽象工廠對象創(chuàng)建完成過后就立即返回整個產(chǎn)品簇;而生成器模式的主要目的是按照構(gòu)造算法,一步一步來構(gòu)建一個復(fù)雜的產(chǎn)品對象,通常要等到整個構(gòu)建過程結(jié)束過后,才會得到最終的產(chǎn)品對象。
事實上,這兩個模式是可以組合使用的,在生成器模式的Builder實現(xiàn)中,需要創(chuàng)建各個部件對象,而這些部件對象是有關(guān)聯(lián)的,通常是構(gòu)成一個復(fù)雜對象的部件對象,也就是說,Builder實現(xiàn)中,需要獲取構(gòu)成一個復(fù)雜對象的產(chǎn)品簇,那自然就可以使用抽象工廠模式來實現(xiàn)。這樣一來,由抽象工廠模式負(fù)責(zé)了部件對象的創(chuàng)建,Builder實現(xiàn)里面就主要負(fù)責(zé)產(chǎn)品對象整體的構(gòu)建了。
- 生成器模式和模板方法模式
這也是兩個非常類似的模式。初看之下,不會覺得這兩個模式有什么關(guān)聯(lián),但是仔細(xì)一思考,發(fā)現(xiàn)兩個模式在功能上很類似。
模板方法模式主要是用來定義算法的骨架,把算法中某些步驟延遲到子類中實現(xiàn)。再想想生成器模式,Director用來定義整體的構(gòu)建算法,把算法中某些涉及到具體部件對象的創(chuàng)建和裝配的功能,委托給具體的Builder來實現(xiàn)。
雖然生成器不是延遲到子類,是委托給Builder,但那只是具體實現(xiàn)方式上的差別,從實質(zhì)上看兩個模式很類似,都是定義一個固定的算法骨架,然后把算法中的某些具體步驟交給其它類來完成,都能實現(xiàn)整體算法步驟和某些具體步驟實現(xiàn)的分離。
當(dāng)然兩個模式也有很大的區(qū)別:
- 首先是模式的目的,生成器模式是用來構(gòu)建復(fù)雜對象的,而模板方法是用來定義算法骨架,尤其是一些復(fù)雜的業(yè)務(wù)功能的處理算法的骨架;
- 其次是模式的實現(xiàn),生成器模式是采用委托的方法,而模板方法是采用的繼承的方式;
- 另外從使用的復(fù)雜度上,生成器模式需要組合Director和Builder對象,然后才能開始構(gòu)建,要等構(gòu)建完后才能獲得最終的對象,而模板方法就沒有這么麻煩,直接使用子類對象即可。
- 生成器模式和組合模式
這兩個模式可以組合使用。
對于復(fù)雜的組合結(jié)構(gòu),可以使用生成器模式來一步一步構(gòu)建。