一天一個設計模式(三) - 建造者模式(Builder)

零壹技術棧發表於2018-07-13

前言

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

(一). 產品的內部表象

一個產品常有不同的組成成分作為產品的零件,這些零件有可能是物件,也有可能不是物件,他們通常又稱為產品的內部表象(internal representation)。

(二). 物件性質的建造

有些情況下,一個物件會有些重要的性質,在它們沒有正確賦值之前,物件不能作為一個完整的產品使用。比如:一個電子郵件有發件人地址、收件人地址、主題、內容、附件等部分,而在最基本的發件人地址得到賦值之前,這個電子郵件是不可以傳送的。

有些情況下,一個物件的有些性質必須按照某個順序賦值才有意義。在某個性質沒有賦值之前,另一個性質則無法賦值。

這些情況使得性質本身的建造設計到複雜的業務邏輯。設定後,此物件相當於一個有待建造的產品,而物件的這些性質相當於產品的零件,建造產品的過程是建造零件的過程。

由於建造零件的過程很複雜,因此,這些零件的建造過程往往被外部化到另一個成為建造者的物件中,建造者物件返還給客戶端的是一個全部零件都建造完畢的產品物件。

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

正文

建造模式的結構

一天一個設計模式(三) - 建造者模式(Builder)

在這個示意的系統裡,最終產品Product只有兩個零件,即part1part2。相應的構造方法也有兩個,即buildPart1()buildPart2()

同時可以看出本模式涉及到四個角色,它們分別為:

抽象建造者(Builder):

給出一個抽象介面,以規範產品物件的各個組成成分的建造。模式中真正建立產品物件的是具體建造者ConcreteBuilder角色。

具體建造者類必須實現這個介面要求的兩種方法

  1. 一種是產品具體零件建造方法buildPart1()buildPart2()
  2. 另一種是返回構造完成的產品的方法retrieveResult()

一般來說,產品所包含的零件數目建造方法的數目相符。換言之,有多少零件需要建造,就會有多少相應的建造方法。

具體建造者(ContreteBuilder):

擔任這個角色的是抽象建造者在具體業務場景的下的建造實現。這個角色要完成的任務包括:

  1. 實現抽象建造者Builder所宣告的介面,給出一步步完成建立產品例項的操作。
  2. 在建造過程完成後,提供產品的例項

導演者(Director):

擔任這個角色的類呼叫具體建造者角色以建立產品物件。應當指出的是,導演者角色並沒有產品類的具體知識,真正擁有產品類的具體知識的是具體建造者角色。

產品(Product):

產品便是建造中的複雜物件,一般來說,一個系統中會有多於一個的產品類,而且這些產品類並不一定有共同的介面,而完全可以是不相關聯的。

建造模式的示例程式碼

Product.java

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

    @Override
    public String toString() {
        return "Product [part1=" + part1 + ", part2=" + part2 + "]";
    }
}
複製程式碼

Builder.java

/**
 * 抽象建造者角色
 *
 * 提供零件建造方法及返回結果方法
 */
public interface Builder {
    void buildPart1();
    void buildPart2();

    Product retrieveResult();
}
複製程式碼

ConcreteBuilder.java

/**
 * 具體建造者角色
 */
public class ConcreteBuilder implements Builder {

    private Product product = new Product();

    /**
     * 建造零件1
     */
    @Override
    public void buildPart1() {
        product.setPart1("零件分類1,編號:10000");
    }

    /**
     * 建造零件2
     */
    @Override
    public void buildPart2() {
        product.setPart2("零件分類2,編號:20000");
    }

    /**
     * 返回建造後成功的產品
     * @return
     */
    @Override
    public Product retrieveResult() {
        return product;
    }

}
複製程式碼

Director.java

/**
 * 導演者角色
 */
public class Director {
    /**
     * 建立建造者物件
     */
    private Builder builder;

    /**
     * 建構函式,給定建造者物件
     * @param builder 建造者物件
     */
    public Director(Builder builder) {
        this.builder = builder;
    }

    /**
     * 產品構造方法,在該方法內,呼叫產品零件建造方法。
     */
    public Product construct(){
        builder.buildPart1();
        builder.buildPart2();
        // 返回builder建造完成的產品物件
        return builder.construct();
    }
}
複製程式碼

Client.java

public class Client {
    public static void main(String[] args) {
        //建立具體建造者物件
        Builder builder = new ConcreteBuilder();
        //創造導演者角色,給定建造者物件
        Director director = new Director(builder);
        //呼叫導演者角色,建立產品零件。並返回產品建造結果。
        Product product = director.construct();
        System.out.println(product);
    }
}
複製程式碼

上述程式碼完成的具體步驟:

  1. 客戶端建立具體建造者物件;
  2. 將具體建造者物件交給導演者;
  3. 導演者操作建造者物件建造產品零件;
  4. 當產品建立完成後,導演者將產品返回給客戶端。

建造者模式構建複雜物件

考慮這樣一個實際業務應用,要建立一個保險合同的物件,裡面很多屬性的值都有約束,要求建立出來的物件是滿足這些約束規則的。

約束規則如下:

保險合同通常情況下可以和個人簽訂,也可以和某個公司簽訂個,但是一份保險合同不能同時和個人和公司簽訂。這個物件裡有很多類似於這樣的約束,採用建造者模式來構建複雜的物件,通常會對建造者模式進行一定的簡化,因為目標明確,就是建立某個複雜物件,因此做適當的簡化會使得程式更簡介。

具體實現思路如下:

  • 由於是用Builder建造者模式來建立某個物件,因此就沒有必要再定義一個Builder介面,直接提供一個具體的建造類就可以了。
  • 對於建立一個複雜的物件,可能會有很多種不同的選擇和步驟,乾脆去掉導演者Director,把導演者的功能和Client客戶端的功能合併起來,也就是說Client客戶端的功能就相當於導演者,它來指導建造者去構建需要的複雜物件

於是,建造者(Builder)可以抽象到目標產品(Product)的內部,這樣最大的好處對外遮蔽掉具體的建造實現,是示例程式碼如下:

InstranceContract.java

/**
 * 保險合同編號
 */
public class InstranceContract {
    /**
     * 保險合同編號
     */
    private String contractId;
    /**
     * 受保人名稱,此處因為有限制條件:要麼同個人簽訂,要麼同公司簽訂
     * 也就是說,受保人名稱屬性同受保公司名稱屬性不能同時有值。
     */
    private String personName;
    /**
     * 受保公司名稱
     */
    private String companyName;
    /**
     * 開始時間
     */
    private long beginDate;
    /**
     * 結束時間,需要大於開始時間
     */
    private long endDate;
    /**
     * 其他資料
     */
    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("當前正在操作的保險合同編號為【"+this.contractId+"】");
        System.out.println(this);
    }

    @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;

        /**
         * 構造方法
         * @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);
        }
    }
}
複製程式碼

客戶端(Client)、導演者(Director)合併到一個類上面,如下:

public class Client {
    public static void main(String[] args) {
        InstranceContract.ConcreteBuilder builder =
                new InstranceContract.ConcreteBuilder("8888", 1233L, 2253L);

        // 導演者進行組裝
        InstranceContract contract =
                builder.setPersonName("趙四").setOtherData("測試資料").build();

        contract.someOperation();
    }
}
複製程式碼

總結

建造者模式主要適用於如下的業務場景:

  1. 內部結構複雜:

需要生成的產品物件有複雜的內部結構,每一個內部元件本身也可以是複雜物件,也可以僅僅是一個簡單的組成部分。

  1. 屬性順序和依賴:

需要生成的產品物件的屬性相互依賴。建造模式可以制實行一種分步驟順序進行的建造過程。因此,如果產品物件的一個屬性必須在另外一個屬性賦值之後才可以被賦值,那麼,使用建造者模式是一個很好的設計思想。

  1. 屬性獲取過程複雜:

在物件建立過程中會使用到系統中的一些其他物件,這些物件在產品物件的建立過程中不易得到


歡迎關注技術公眾號: 零壹技術棧

零壹技術棧

本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。

相關文章