【轉載】《JAVA與模式》之建造模式

在閻宏博士的《JAVA與模式》一書中開頭是這樣描述建造(Builder)模式的:

  建造模式是對象的創建模式。建造模式可以將一個產品的內部表象(internal representation)與產品的生產過程分割開來,從而可以使一個建造過程生成具有不同的內部表象的產品對象。


產品的內部表象

  一個產品常有不同的組成成分作爲產品的零件,這些零件有可能是對象,也有可能不是對象,它們通常又叫做產品的內部表象(internal representation)。不同的產品可以有不同的內部表象,也就是不同的零件。使用建造模式可以使客戶端不需要知道所生成的產品有哪些零件,每個產品的對應零件彼此有何不同,是怎麼建造出來的,以及怎麼組成產品。

對象性質的建造

  有些情況下,一個對象會有一些重要的性質,在它們沒有恰當的值之前,對象不能作爲一個完整的產品使用。比如,一個電子郵件有發件人地址、收件人地址、主題、內容、附錄等部分,而在最起碼的收件人地址得到賦值之前,這個電子郵件不能發送。

  有些情況下,一個對象的一些性質必須按照某個順序賦值纔有意義。在某個性質沒有賦值之前,另一個性質則無法賦值。這些情況使得性質本身的建造涉及到複雜的商業邏輯。這時候,此對象相當於一個有待建造的產品,而對象的這些性質相當於產品的零件,建造產品的過程是建造零件的過程。由於建造零件的過程很複雜,因此,這些零件的建造過程往往被“外部化”到另一個稱做建造者的對象裏,建造者對象返還給客戶端的是一個全部零件都建造完畢的產品對象。

  建造模式利用一個導演者對象和具體建造者對象一個個地建造出所有的零件,從而建造出完整的產品對象。建造者模式將產品的結構和產品的零件的建造過程對客戶端隱藏起來,把對建造過程進行指揮的責任和具體建造者零件的責任分割開來,達到責任劃分和封裝的目的。 

建造模式的結構

  

  在這個示意性的系統裏,最終產品Product只有兩個零件,即part1和part2。相應的建造方法也有兩個:buildPart1()和buildPart2()、同時可以看出本模式涉及到四個角色,它們分別是:

  抽象建造者(Builder)角色:給 出一個抽象接口,以規範產品對象的各個組成成分的建造。一般而言,此接口獨立於應用程序的商業邏輯。模式中直接創建產品對象的是具體建造者 (ConcreteBuilder)角色。具體建造者類必須實現這個接口所要求的兩種方法:一種是建造方法(buildPart1和 buildPart2),另一種是返還結構方法(retrieveResult)。一般來說,產品所包含的零件數目與建造方法的數目相符。換言之,有多少 零件,就有多少相應的建造方法。

  具體建造者(ConcreteBuilder)角色:擔任這個角色的是與應用程序緊密相關的一些類,它們在應用程序調用下創建產品的實例。這個角色要完成的任務包括:1.實現抽象建造者Builder所聲明的接口,給出一步一步地完成創建產品實例的操作。2.在建造過程完成後,提供產品的實例。

  導演者(Director)角色:擔任這個角色的類調用具體建造者角色以創建產品對象。應當指出的是,導演者角色並沒有產品類的具體知識,真正擁有產品類的具體知識的是具體建造者角色。

  產品(Product)角色:產品便是建造中的複雜對象。一般來說,一個系統中會有多於一個的產品類,而且這些產品類並不一定有共同的接口,而完全可以是不相關聯的。

  

  導演者角色是與客戶端打交道的角色。導演者將客戶端創建產品的請求劃分爲對各個零件的建造請求,再將這些請求委派給具體建造者角色。具體建造者角色是做具體建造工作的,但是卻不爲客戶端所知。

  一般來說,每有一個產品類,就有一個相應的具體建造者類。這些產品應當有一樣數目的零件,而每有一個零件就相應地在所有的建造者角色裏有一個建造方法。

源代碼

 產品類Product

複製代碼
public class Product {
/**
* 定義一些關於產品的操作
*/
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;
}
}
複製代碼

 抽象建造者類Builder

複製代碼
public interface Builder {
public void buildPart1();
public void buildPart2();
public Product retrieveResult();
}
複製代碼

  具體建造者類ConcreteBuilder

複製代碼
public class ConcreteBuilder implements Builder {

private Product product = new Product();
/**
* 產品零件建造方法1
*/
@Override
public void buildPart1() {
//構建產品的第一個零件
    product.setPart1("編號:9527");
}
/**
* 產品零件建造方法2
*/
@Override
public void buildPart2() {
//構建產品的第二個零件
    product.setPart2("名稱:XXX");
}
/**
* 產品返還方法
*/
@Override
public Product retrieveResult() {
return product;
}

}
複製代碼

   導演者類Director

複製代碼
public class Director {
/**
* 持有當前需要使用的建造器對象
*/
private Builder builder;
/**
* 構造方法,傳入建造器對象
*
@param builder 建造器對象
*/
public Director(Builder builder){
this.builder = builder;
}
/**
* 產品構造方法,負責調用各個零件建造方法
*/
public void construct(){
builder.buildPart1();
builder.buildPart2();
}
}
複製代碼

  客戶端類Client

複製代碼
public class Client {
public static void main(String[]args){
Builder builder = new ConcreteBuilder();
Director director = new Director(builder);
director.construct();
Product product = builder.retrieveResult();
System.out.println(product.getPart1());
System.out.println(product.getPart2());
}
}
複製代碼

時序圖

  客戶端負責創建導演者和具體建造者對象。然後,客戶端把具體建造者對象交給導演者,導演者操作具體建造者,開始創建產品。當產品完成後,建造者把產品返還給客戶端。

  把創建具體建造者對象的任務交給客戶端而不是導演者對象,是爲了將導演者對象與具體建造者對象的耦合變成動態的,從而使導演者對象可以操縱數個具體建造者對象中的任何一個。


 

使用場景

  假設有一個電子雜誌系統,定期地向用戶的電子郵件信箱發送電子雜誌。用戶可以通過網頁訂閱電子雜誌,也可以通過網頁結束訂閱。當客戶開始訂閱時,系統發送一個電子郵件表示歡迎,當客戶結束訂閱時,系統發送一個電子郵件表示歡送。本例子就是這個系統負責發送“歡迎”和“歡送”郵件的模塊。

  在本例中,產品類就是發給某個客戶的“歡迎”和“歡送”郵件,如下圖所示。

  雖然在這個例子裏面各個產品類均有一個共同的接口,但這僅僅是本例子特有的,並不代表建造模式的特點。建造模式可以應用到具有完全不同接口的產品類上。大多數情況下是不知道最終構建出來的產品是什麼樣的,所以在標準的建造模式裏面,一般是不需要對產品定義抽象接口的,因爲最終構造的產品千差萬別,給這些產品定義公共接口幾乎是沒有意義的。

  下圖所示就是這個系統的類圖。

  

  這個系統含有客戶端(Client)、導演者(Director)、抽象建造者(Builder)、具體建造者(WelcomeBuilder和GoodbyeBuilder)、產品(WelcomeMessage和GoodbyeMessage)等角色。

源代碼

  抽象類AutoMessage源代碼,send()操作僅僅是示意性的,並沒有給出任何發送電子郵件的代碼。

複製代碼
public abstract class AutoMessage {
//收件人地址
private String to;
//發件人地址
private String from;
//標題
private String subject;
//內容
private String body;
//發送日期
private Date sendDate;
public void send(){
System.out.println("收件人地址:" + to);
System.out.println("發件人地址:" + from);
System.out.println("標題:" + subject);
System.out.println("內容:" + body);
System.out.println("發送日期:" + 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;
}

}
複製代碼

  具體產品類WelcomeMessage

複製代碼
public class WelcomeMessage extends AutoMessage {
/**
* 構造子
*/
public WelcomeMessage(){
System.out.println("發送歡迎信息");
}
}
複製代碼

  具體產品類GoodbyeMessage

複製代碼
public class GoodbyeMessage extends AutoMessage{
/**
* 構造子
*/
public GoodbyeMessage(){
System.out.println("發送歡送信息");
}
}
複製代碼

  抽象建造者類

複製代碼
public abstract class Builder {
protected AutoMessage msg;
//標題零件的建造方法
public abstract void buildSubject();
//內容零件的建造方法
public abstract void buildBody();
//收件人零件的建造方法
public void buildTo(String to){
msg.setTo(to);
}
//發件人零件的建造方法
public void buildFrom(String from){
msg.setFrom(from);
}
//發送時間零件的建造方法
public void buildSendDate(){
msg.setSendDate(new Date());
}
/**
* 郵件產品完成後,用此方法發送郵件
* 此方法相當於產品返還方法
*/
public void sendMessage(){
msg.send();
}
}
複製代碼

  具體建造者WelcomeBuilder

複製代碼
public class WelcomeBuilder extends Builder {
public WelcomeBuilder(){
msg = new WelcomeMessage();
}
@Override
public void buildBody() {
// TODO Auto-generated method stub
    msg.setBody("歡迎內容");
}

@Override
public void buildSubject() {
// TODO Auto-generated method stub
    msg.setSubject("歡迎標題");
}

}
複製代碼

  具體建造者GoodbyeBuilder

複製代碼
public class GoodbyeBuilder extends Builder {

public GoodbyeBuilder(){
msg = new GoodbyeMessage();
}
@Override
public void buildBody() {
// TODO Auto-generated method stub
    msg.setBody("歡送內容");
}

@Override
public void buildSubject() {
// TODO Auto-generated method stub
    msg.setSubject("歡送標題");
}

}
複製代碼

  導演者Director,這個類提供一個construct()方法,此方法調用建造者的建造方法,包括buildTo()、buildFrom()、buildSubject()、buildBody()、buildSendDate()等,從而一部分一部分地建造出產品對象,既AutoMessage對象。

複製代碼
public class Director {
Builder builder;
/**
* 構造子
*/
public Director(Builder builder){
this.builder = builder;
}
/**
* 產品構造方法,負責調用各零件的建造方法
*/
public void construct(String toAddress , String fromAddress){
this.builder.buildTo(toAddress);
this.builder.buildFrom(fromAddress);
this.builder.buildSubject();
this.builder.buildBody();
this.builder.buildSendDate();
this.builder.sendMessage();
}
}
複製代碼

  客戶端Client

複製代碼
public class Client {

public static void main(String[] args) {
// TODO Auto-generated method stub
    Builder builder = new WelcomeBuilder();
Director director = new Director(builder);
director.construct("[email protected]", "[email protected]");

}

}
複製代碼

  建造模式分成兩個很重要的部分:

  1. 一個部分是Builder接口,這裏是定義瞭如何構建各個部件,也就是知道每個部件功能如何實現,以及如何裝配這些部件到產品中去;

  2. 另外一個部分是Director,Director是知道如何組合來構建產品,也就是說Director負責整體的構建算法,而且通常是分步驟地來執行。

  不管如何變化,建造模式都存在這麼兩個部分,一個部分是部件構造和產品裝配,另一個部分是整體構建的算法。認識這點是很重要的,因爲在建造模式中,強調的是固定整體構建的算法,而靈活擴展和切換部件的具體構造和產品裝配的方式。

  再直白點說,建造模式的重心在於分離構建算法和具體的構造實現,從而使得構建算法可以重用。具體的構造實現可以很方便地擴展和切換,從而可以靈活地組合來構造出不同的產品對象。

 


使用建造模式構建複雜對象

  考慮這樣一個實際應用,要創建一個保險合同的對象,裏面很多屬性的值都有約束,要求創建出來的對象是滿足這些約束規則的。約束規則比如:保險合同通常情況下可以和個人簽訂,也可以和某個公司簽訂,但是一份保險合同不能同時與個人和公司簽訂。這個對象裏有很多類似這樣的約束,採用建造模式來構建複雜的對象,通常會對建造模式進行一定的簡化,因爲目標明確,就是創建某個複雜對象,因此做適當簡化會使程序更簡潔。大致簡化如下:

  ●  由於是用Builder模式來創建某個對象,因此就沒有必要再定義一個Builder接口,直接提供一個具體的建造者類就可以了。

  ●  對於創建一個複雜的對象,可能會有很多種不同的選擇和步驟,乾脆去掉“導演者”,把導演者的功能和Client的功能合併起來,也就是說,Client這個時候就相當於導演者,它來指導構建器類去構建需要的複雜對象。

  保險合同類

複製代碼
/**
* 保險合同對象
*/
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 void someOperation(){
System.out.println("當前正在操作的保險合同編號爲【"+this.contractId+"】");
}

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;
}
//被保險人員的名稱
    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;
}
/**
* 構建真正的對象並返回
*
@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 class Client {
public static void main(String[]args){
//創建構建器對象
    InsuranceContract.ConcreteBuilder builder =
new InsuranceContract.ConcreteBuilder("9527", 123L, 456L);
//設置需要的數據,然後構建保險合同對象
    InsuranceContract contract =
builder.setPersonName("小明").setOtherData("test").build();
//操作保險合同對象的方法
    contract.someOperation();
}
}
複製代碼

  在本例中將具體建造者合併到了產品對象中,並將產品對象的構造函數私有化,防止客戶端不使用構建器來構建產品對象,而是直接去使用new來構建產品對象所導致的問題。另外,這個構建器的功能就是爲了創建被構建的對象,完全可以不用單獨一個類。


在什麼情況下使用建造模式

  1. 需要生成的產品對象有複雜的內部結構,每一個內部成分本身可以是對象,也可以僅僅是一個對象(即產品對象)的一個組成部分。

  2. 需要生成的產品對象的屬性相互依賴。建造模式可以強制實行一種分步驟進行的建造過程,因此,如果產品對象的一個屬性必須在另一個屬性被賦值之後纔可以被賦值,使用建造模式是一個很好的設計思想。

  3. 在對象創建過程中會使用到系統中的其他一些對象,這些對象在產品對象的創建過程中不易得到。

轉載地址:http://www.cnblogs.com/java-my-life/archive/2012/03/28/2418836.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章