定義
建造模式是對象的創(chuàng)建模式。建造模式可以將一個產(chǎn)品的內(nèi)部表象(internal representation)與產(chǎn)品的生產(chǎn)過程分割開來,從而可以使一個建造過程生成具有不同的內(nèi)部表象的產(chǎn)品對象。
產(chǎn)品的內(nèi)部表象
一個產(chǎn)品常有不同的組成成分作為產(chǎn)品的零件,這些零件有可能是對象,也有可能不是對象,他們通常又稱為產(chǎn)品的內(nèi)部表象(internal representation)。
不同的產(chǎn)品可以有不同的內(nèi)部表象,也就是不同的零件。使用建造模式可以使客戶端不需要知道所生產(chǎn)的產(chǎn)品有哪些零件,每個產(chǎn)品的對應(yīng)零件有何不同,是如何建造出來的,以及如何組成產(chǎn)品。
對象性質(zhì)的建造
有些情況下,一個對象會有一些重要的性質(zhì),在它們沒有恰當(dāng)?shù)闹抵埃瑢ο蟛荒茏鳛橐粋€完整的產(chǎn)品使用。比如:一個電子郵件有發(fā)件人地址、收件人地址、主題、內(nèi)容、附件等部分,而在最基本的發(fā)件人地址得到賦值之前,這個電子郵件是不可以發(fā)送的。
有些情況下,一個對象的有些性質(zhì)必須按照某個順序賦值才有意義。在某個性質(zhì)沒有賦值之前,另一個性質(zhì)則無法賦值。這些情況使得性質(zhì)本身的建造設(shè)計到復(fù)雜的商業(yè)邏輯。設(shè)置后,此對象相當(dāng)于一個有待建造的產(chǎn)品,而對象的這些性質(zhì)相當(dāng)于產(chǎn)品的零件,建造產(chǎn)品的過程是建造零件的過程。由于建造零件的過程很復(fù)雜,因此,這些零件的建造過程往往被“外部化”到另一個成為建造者的對象中,建造者對象返還給客戶端的是一個全部零件都建造完畢的產(chǎn)品對象。
建造模式利用一個導(dǎo)演者對象和具體建造者對象一個個的建造出所有的零件,從而建造出完整的產(chǎn)品對象。建造者模式將產(chǎn)品的結(jié)構(gòu)和產(chǎn)品的零件的建造過程對客戶端隱藏起來,把對建造過程進(jìn)行指揮的責(zé)任和具體建造者零件的責(zé)任分割開來,達(dá)到責(zé)任劃分和封裝的目的。
建造模式的結(jié)構(gòu)
在這個示意性的系統(tǒng)里,最終產(chǎn)品Product
只有兩個零件,即part1
和part2
。相應(yīng)的構(gòu)造方法也有兩個:buildPart1()
和buildPart2()
。同時可以看出本模式涉及到四個角色,他們分別為:
-
抽象建造者(Builder):給出一個抽象接口,以規(guī)范產(chǎn)品對象的各個組成成分的建造。一般而言,此接口獨(dú)立于應(yīng)用程序的商業(yè)邏輯。模式中直接創(chuàng)建產(chǎn)品對象的是具體建造者
ConcreteBuilder
角色。具體建造者類必須實(shí)現(xiàn)這個接口要求的兩種方法:一種是建造方法,buildPart1()
和buildPart2()
;另一種是返回結(jié)構(gòu)方法retrieveResult()
。一般來說,產(chǎn)品所包含的零件數(shù)目與建造方法的數(shù)目相符。換言之,有多少零件需要建造,就會有多少相應(yīng)的建造方法。 -
具體建造者(ContreteBuilder):擔(dān)任這個角色的是與應(yīng)用程序緊密相關(guān)的一些類,他們在應(yīng)用程序調(diào)用下創(chuàng)建產(chǎn)品的實(shí)例。這個角色要完成的任務(wù)包括:
- 實(shí)現(xiàn)抽象建造者
Builder
所聲明的接口,給出一步步完成創(chuàng)建產(chǎn)品實(shí)例的操作。 - 在建造過程完成后,提供產(chǎn)品的實(shí)例。
- 實(shí)現(xiàn)抽象建造者
- 導(dǎo)演者(Director):擔(dān)任這個角色的類調(diào)用具體建造者角色以創(chuàng)建產(chǎn)品對象。應(yīng)當(dāng)指出的是,導(dǎo)演者角色并沒有產(chǎn)品類的具體知識,真正擁有產(chǎn)品類的具體知識的是具體建造者角色。
- 產(chǎn)品(Product):產(chǎn)品便是建造中的復(fù)雜對象,一半來說,一個系統(tǒng)中會有多于一個的產(chǎn)品類,而且這些產(chǎn)品類并不一定有共同的借口,而完全可以是不相關(guān)聯(lián)的。
導(dǎo)演者角色是與客戶端打交道的角色,導(dǎo)演者將客戶端創(chuàng)建產(chǎn)品的請求劃分為對各個零件的建造請求,再將這些請求委派給具體建造者角色。
具體建造者角色是做具體建造工作的,但是卻對客戶端透明。
一般來說,每有一個產(chǎn)品類,就有一個相應(yīng)的具體建造者類。這些產(chǎn)品應(yīng)當(dāng)有一樣數(shù)目的零件,而每有一個零件就相應(yīng)的在所有的建造者角色中有一個建造方法。
示例代碼
產(chǎn)品類Product
public class Product {
/**
* 產(chǎn)品零件
*/
private String part1;
private String part2;
public String getPart1() {
return part1;
}
public void setPart1(String part1) {
this.part1 = part1;
}
public String getPart2() {
return part2;
}
public void setPart2(String part2) {
this.part2 = part2;
}
@Override
public String toString() {
return "Product [part1=" + part1 + ", part2=" + part2 + "]";
}
}
抽象建造者接口Builder
/**
* 抽象建造者角色
*
* 提供零件建造方法及返回結(jié)果方法
*/
public interface Builder {
void buildPart1();
void buildPart2();
Product retrieveResult();
}
具體建造者角色類ConcreteBuilder
/**
* 具體建造者角色
*/
public class ConcreteBuilder implements Builder {
private Product product = new Product();
/**
* 建造零件1
*/
@Override
public void buildPart1() {
product.setPart1("編號:9999");
}
/**
* 建造零件2
*/
@Override
public void buildPart2() {
product.setPart2("名稱:建造攻城獅");
}
/**
* 返回建造后成功的產(chǎn)品
* @return
*/
@Override
public Product retrieveResult() {
return product;
}
}
導(dǎo)演者角色類
/**
* 導(dǎo)演者角色
*/
public class Director {
/**
* 創(chuàng)建建造者對象
*/
private Builder builder;
/**
* 構(gòu)造函數(shù),給定建造者對象
* @param builder 建造者對象
*/
public Director(Builder builder) {
this.builder = builder;
}
/**
* 產(chǎn)品構(gòu)造方法,在該方法內(nèi),調(diào)用產(chǎn)品零件建造方法。
*/
public void construct(){
builder.buildPart1();
builder.buildPart2();
}
}
客戶端類Client
/*
* 客戶端
*/
public class Client {
public static void main(String[] args) {
//創(chuàng)建具體建造者對象
Builder builder = new ConcreteBuilder();
//創(chuàng)造導(dǎo)演者角色,給定建造者對象
Director director = new Director(builder);
//調(diào)用導(dǎo)演者角色,創(chuàng)建產(chǎn)品零件
director.construct();
//接收建造者角色產(chǎn)品建造結(jié)果
Product product = builder.retrieveResult();
System.out.println(product.toString());
}
}
客戶端創(chuàng)建具體建造者對象,然后將具體建造者對象交給導(dǎo)演者角色,導(dǎo)演者操作建造者對象建造產(chǎn)品零件,當(dāng)產(chǎn)品創(chuàng)建完成后,建造者將產(chǎn)品返回給客戶端。
將創(chuàng)建具體建造者對象的任務(wù)交給客戶端,而不是導(dǎo)演者對象的原因,是為了將導(dǎo)演者對象同具體建造者對象的耦合變成動態(tài)的,從而使得導(dǎo)演者對象可以操作多個具體建造者對象中的任何一個。
使用場景
假設(shè)有一個電子雜志系統(tǒng),定期的向用戶的電子郵箱發(fā)送電子雜志,用戶可以通過網(wǎng)頁訂閱電子雜志,也可以通過網(wǎng)頁結(jié)束訂閱。
當(dāng)用戶開始訂閱時,系統(tǒng)發(fā)送一個電子郵件表示歡迎,當(dāng)客戶結(jié)束訂閱時,系統(tǒng)發(fā)送一個電子郵件表示歡送。
本例子就是這個系統(tǒng)中負(fù)責(zé)發(fā)送“歡迎”和“歡送”郵件的模塊。
本例子中,產(chǎn)品類就是發(fā)送給某個客戶的郵件,如下圖所示
雖然在這個例子里面每個產(chǎn)品類均有一個共同的接口,但這僅為本例子特有的,并不代表建造者模式的特點(diǎn)。
建造者模式可以應(yīng)用到具有完全不同接口的產(chǎn)品類上。大多數(shù)情況下是不知道最終構(gòu)建出來的產(chǎn)品是什么樣的,所以在標(biāo)準(zhǔn)的建造者模式里面,一般對產(chǎn)品是不需要定義抽象接口的,因?yàn)樽罱K建造的產(chǎn)品完全不一樣,給這些產(chǎn)品定義公共抽象接口幾乎是沒有任何意義的。
下圖所示就是這個系統(tǒng)的類圖:
這個系統(tǒng)包含有客戶端(Client)、導(dǎo)演者(Director)、抽象建造者(Builder)、具體建造者(WelcomeBuilder和GoodbyeBuilder)、產(chǎn)品(WelcomeMessage和GoodbyeMessage)等角色。
示例代碼
抽象類AutoMessage源代碼,send()方法在此處為測試方法,不進(jìn)行正式發(fā)郵件操作。
import java.util.Date;
public abstract class AutoMessage {
/**
* 收件人地址
*/
private String to;
/**
* 發(fā)件人地址
*/
private String from;
/**
* 標(biāo)題
*/
private String subject;
/**
* 內(nèi)容
*/
private String body;
/**
* 發(fā)送日期
*/
private Date sendDate;
public void send() {
System.out.println("收件人地址:" + to);
System.out.println("發(fā)件人地址:" + from);
System.out.println("標(biāo)題:" + subject);
System.out.println("內(nèi)容:" + body);
System.out.println("發(fā)送日期:" + sendDate);
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public Date getSendDate() {
return sendDate;
}
public void setSendDate(Date sendDate) {
this.sendDate = sendDate;
}
}
具體歡迎產(chǎn)品類WelcomeMessage
public class WelcomeMessage extends AutoMessage {
/**
* 構(gòu)造函數(shù)
*/
public WelcomeMessage() {
System.out.println("發(fā)送歡迎信息");
}
}
具體歡送產(chǎn)品類GoodbyeMessage
public class GoodbyeMessage extends AutoMessage {
/**
* 構(gòu)造函數(shù)
*/
public GoodbyeMessage() {
System.out.println("發(fā)送歡送信息");
}
}
抽象建造者類Builder
import java.util.Date;
public abstract class Builder {
protected AutoMessage message;
/**
* 建造標(biāo)題零件的方法
*/
public abstract void buildSubject();
/**
* 建造內(nèi)容零件的方法
*/
public abstract void buildBody();
/**
* 建造收件人零件
*/
public void buildTo(String to) {
message.setTo(to);
}
/**
* 建造發(fā)件人零件
*/
public void buildFrom(String from) {
message.setFrom(from);
}
/**
* 建造發(fā)送時間零件
*/
public void buildSendDate() {
message.setSendDate(new Date());
}
/**
* 郵件構(gòu)建完成后,調(diào)用方法發(fā)送郵件
* 相當(dāng)于建造者角色產(chǎn)品返回方法。
*/
public void sendMessge() {
message.send();
}
}
具體歡迎建造者類WelcomeBuilder
public class WelcomeBuilder extends Builder {
public WelcomeBuilder() {
message = new WelcomeMessage();
}
@Override
public void buildSubject() {
message.setSubject("歡迎標(biāo)題");
}
@Override
public void buildBody() {
message.setBody("歡迎內(nèi)容");
}
}
具體歡送建造者類GoodbyeBuilder
public class GoodbyeBuilder extends Builder {
public GoodbyeBuilder() {
message = new GoodbyeMessage();
}
@Override
public void buildSubject() {
message.setSubject("歡送標(biāo)題");
}
@Override
public void buildBody() {
message.setBody("歡送正文");
}
}
導(dǎo)演者Director,這個類提供一個construct()方法,此方法調(diào)用建造者的建造方法,包括各個零件的建造方法,從而逐個零件的建設(shè)對象。
public class Director {
Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
public void construct(String toAddress, String fromAdress) {
this.builder.buildTo(toAddress);
this.builder.buildFrom(fromAdress);
this.builder.buildSubject();
this.builder.buildBody();
this.builder.buildSendDate();
}
}
客戶端Client
public class Client {
public static void main(String[] args) {
Builder builder = new GoodbyeBuilder();
Director director = new Director(builder);
director.construct("to@126.com", "from@126.com");
builder.sendMessge();
}
}
執(zhí)行結(jié)果為:
發(fā)送歡送信息
收件人地址:to@126.com
發(fā)件人地址:from@126.com
標(biāo)題:歡送標(biāo)題
內(nèi)容:歡送正文
發(fā)送日期:Thu Jun 22 18:14:55 CST 2017
建造者模式中有兩個非常重要的部分:
- 一個部分是Builder接口,也就是建造者結(jié)構(gòu)。這里定義了如何構(gòu)建各個部件,也就是知道每個部件功能如何實(shí)現(xiàn),以及如何裝配這些部件到產(chǎn)品中去。
- 另外一個部分是Director導(dǎo)演者角色。導(dǎo)演者知道如何組合來建造產(chǎn)品,也就是Director負(fù)責(zé)產(chǎn)品的整體構(gòu)建,通常是分步驟的來執(zhí)行。
不管如何變化,建造模式都存在這么兩個部分,一個部分是部件構(gòu)造方法和產(chǎn)品裝配,另一個部分是整體構(gòu)建的算法。認(rèn)識這點(diǎn)是很重要的,因?yàn)樵诮ㄔ煺吣J街校瑥?qiáng)調(diào)的是固定整體構(gòu)建的算法,而靈活擴(kuò)展和切換部件的是具體建造者角色和產(chǎn)品裝配的方式。
也就是說,建造者模式的中心在于分離構(gòu)造算法和具體的構(gòu)造實(shí)現(xiàn),從而使得建造算法可以重用,具體的構(gòu)造實(shí)現(xiàn)可以很方便的進(jìn)行拓展和切換,從而可以靈活的組合建造不同的產(chǎn)品對象。
使用建造者模式構(gòu)建復(fù)雜對象
考慮這樣一個世紀(jì)應(yīng)用,要創(chuàng)建一個保險合同的對象,里面很多屬性的值都有約束,要求創(chuàng)建出來的對象是滿足這些約束規(guī)則的。
約束規(guī)則例如:保險合同通常情況下可以和個人簽訂,也可以和某個公司簽訂個,但是一份保險合同不能同時和個人和公司簽訂。這個對象里有很多類似于這樣的約束,采用建造者模式來構(gòu)建復(fù)雜的對象,通常會對建造者模式進(jìn)行一定的簡化,因?yàn)槟繕?biāo)明確,就是創(chuàng)建某個復(fù)雜對象,因此做適當(dāng)?shù)暮喕瘯沟贸绦蚋喗椤?/p>
大致簡化如下:
- 由于是用Builder建造者模式來創(chuàng)建某個對象,因此就沒有必要再定義一個Builder接口,直接提供一個具體的建造類就可以了。
- 對于創(chuàng)建一個復(fù)雜的對象,可能會有很多種不同的選擇和步驟,干脆去掉導(dǎo)演者Director,把導(dǎo)演者的功能和Client客戶端的功能合并起來,也就是說Client客戶端的功能就相當(dāng)于導(dǎo)演者,它來指導(dǎo)建造者去構(gòu)建需要的復(fù)雜對象。
保險合同及建造者角色類
/**
* 保險合同編號
*/
public class InstranceContract {
/**
* 保險合同編號
*/
private String contractId;
/**
* 受保人名稱,此處因?yàn)橛邢拗茥l件:要么同個人簽訂,要么同公司簽訂
* 也就是說,受保人名稱屬性同受保公司名稱屬性不能同時有值。
*/
private String personName;
/**
* 受保公司名稱
*/
private String companyName;
/**
* 開始時間
*/
private long beginDate;
/**
* 結(jié)束時間,需要大于開始時間
*/
private long endDate;
/**
* 其他數(shù)據(jù)
*/
private String otherData;
private InstranceContract(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 void someOperation(){
System.out.println("當(dāng)前正在操作的保險合同編號為【"+this.contractId+"】");
System.out.println(toString());
}
@Override
public String toString() {
return "InstranceContract [contractId=" + contractId + ", personName=" + personName + ", companyName="
+ companyName + ", beginDate=" + beginDate + ", endDate=" + endDate + ", otherData=" + otherData + "]";
}
public static class ConcreteBuilder {
private String contractId;
private String personName;
private String companyName;
private long beginDate;
private long endDate;
private String otherData;
/**
* 構(gòu)造方法
* @param contractId 保險合同編號
* @param beginDate 生效時間
* @param endDate 失效時間
*/
public ConcreteBuilder(String contractId, long beginDate, long endDate) {
this.contractId = contractId;
this.beginDate = beginDate;
this.endDate = endDate;
}
public ConcreteBuilder setPersonName(String personName) {
this.personName = personName;
return this;
}
public ConcreteBuilder setCompanyName(String companyName) {
this.companyName = companyName;
return this;
}
public ConcreteBuilder setOtherData(String otherData) {
this.otherData = otherData;
return this;
}
public InstranceContract 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 && !signCompany) {
throw new IllegalArgumentException("一份保險合同不能沒有簽訂對象");
}
if (beginDate <= 0) {
throw new IllegalArgumentException("一份保險合同必須有生效的日期");
}
if (endDate <= 0) {
throw new IllegalArgumentException("一份保險合同必須有失效的日期");
}
if (endDate <= beginDate) {
throw new IllegalArgumentException("一份保險合同的失效日期必須要大于生效的日期");
}
return new InstranceContract(this);
}
}
}
客戶端
public class Client {
public static void main(String[] args) {
InstranceContract.ConcreteBuilder builder =
new InstranceContract.ConcreteBuilder("8888", 1233L, 2253L);
InstranceContract contract =
builder.setPersonName("趙四").setOtherData("測試數(shù)據(jù)").build();
contract.someOperation();
}
}
在本例子中,我們將建造者角色合并到產(chǎn)品對象中,作為產(chǎn)品對象的內(nèi)部類來使用。同時,我們將產(chǎn)品的構(gòu)造函數(shù)私有化,防止用戶直接使用構(gòu)造函數(shù)創(chuàng)造對象。因?yàn)樵谶@個例子中,建造者角色的主要目的就是構(gòu)建合同對象,因此,可以不用創(chuàng)建單獨(dú)的類。
在什么情況下使用建造者模式
- 需要生成的產(chǎn)品對象有復(fù)雜的內(nèi)部結(jié)構(gòu),每一個內(nèi)部成分本身也可以是對象,也可以僅僅是一個對象(產(chǎn)品)的一個組成部分。
- 需要生成的產(chǎn)品對象的屬性相互依賴。建造模式可以強(qiáng)制實(shí)行一種分步驟進(jìn)行的建造過程。因此,如果產(chǎn)品對象的一個屬性必須在另外一個屬性賦值之后才可以被賦值,那么,使用建造者模式是一個很好的設(shè)計思想。
- 在對象創(chuàng)建過程中會使用到系統(tǒng)中的一些其他對象,這些對象在產(chǎn)品對象的創(chuàng)建過程中不易得到。