重識設計模式-建造者模式(Builder Pattern)

Coding小僧發表於2019-03-17

本文已同步發表到我的技術微信公眾號,掃一掃文章底部的二維碼或在微信搜尋 “程式設計師驛站”即可關注,不定期更新優質技術文章。同時,也歡迎加入QQ技術群(群號:650306310)一起交流學習!

定義

建造者模式(Builder Pattern): 將一個複雜物件的構建與它的表示分離,使得同樣的構建過程可以建立不同的表示。建造者模式是一種物件建立型模式。

建造者模式將客戶端與包含多個組成部分(或部件)的複雜物件的建立過程分離,客戶端無須知道複雜物件的內部組成部分與裝配方式,只需要知道所需建造者的型別即可。它關注如何一步一步建立一個的複雜物件,不同的具體建造者定義了不同的建立過程,且具體建造者相互獨立,增加新的建造者非常方便,無須修改已有程式碼,系統具有較好的擴充套件性。

建造者模式主要解決在軟體系統中,有時候面臨著"一個複雜物件"的建立工作,其通常由各個部分的子物件用一定的演算法構成;由於需求的變化,這個複雜物件的各個部分經常面臨著劇烈的變化,但是將它們組合在一起的演算法卻相對穩定。是在一些基本部件不會變,而其組合經常變化的時候使用。

角色

建造者模式一步一步建立一個複雜的物件,它允許使用者只通過指定複雜物件的型別和內容就可以構建它們,使用者不需要知道內部的具體構建細節。建造者模式結構如圖所示:

建造者模式結構圖

在建造者模式結構圖中包含如下幾個角色:
●Builder(抽象建造者): 它為建立一個產品Product物件的各個部件指定抽象介面,在該介面中一般宣告兩類方法,一類方法是buildPartX(),它們用於建立複雜物件的各個部件;另一類方法是getResult(),它們用於返回複雜物件。Builder既可以是抽象類,也可以是介面。

●ConcreteBuilder(具體建造者): 它實現了Builder介面,實現各個部件的具體構造和裝配方法,定義並明確它所建立的複雜物件,也可以提供一個方法返回建立好的複雜產品物件。

●Product(產品角色): 它是被構建的複雜物件,包含多個組成部件,具體建造者建立該產品 的內部表示並定義它的裝配過程。

●Director(指揮者): 指揮者又稱為導演類,它負責安排複雜物件的建造次序,指揮者與抽象建造者之間存在關聯關係,可以在其construct()建造方法中呼叫建造者物件的部件構造與裝配方法,完成複雜物件的建造。客戶端一般只需要與指揮者進行互動,在客戶端確定具體建造者的型別,並例項化具體建造者物件(也可以通過配置檔案和反射機制),然後通過指揮者類的建構函式或者Setter方法將該物件傳入指揮者類中。

在建造者模式的定義中提到了複雜物件,那麼什麼是複雜物件?簡單來說,複雜物件是指那些包含多個成員屬性的物件,這些成員屬性也稱為部件或零件,如汽車包括方向盤、發動機、輪胎等部件,電子郵件包括髮件人、收件人、主題、內容、附件等部件,一個典型的複雜物件類程式碼示例如下:

/**
 * Created by Coding小僧 on 2019/3/17
 *
 * @since 1.0
 */
public class Product {
    private String partA; //定義部件,部件可以是任意型別,包括值型別和引用
    private String partB;
    private String partC;
    //partA的Getter方法和Setter方法省略
    //partB的Getter方法和Setter方法省略
    //partC的Getter方法和Setter方法省略
}
複製程式碼

在抽象建造者類中定義了產品的建立方法和返回方法,其典型程式碼如下:

/**
 * Created by Coding小僧 on 2019/3/17
 *
 * @since 1.0
 */
public abstract class Builder {
    //建立產品物件
    protected Product product = new Product();

    public abstract void buildPartA();

    public abstract void buildPartB();

    public abstract void buildPartC();

    //返回產品物件
    public Product getResult() {
        return product;
    }
}
複製程式碼

在抽象類Builder中宣告瞭一系列抽象的buildPartX()方法用於建立複雜產品的各個部件,具體建造過程在ConcreteBuilder中實現,此外還提供了工廠方法getResult(),用於返回一個建造好的完整產品。

在ConcreteBuilder中實現了buildPartX()方法,通過呼叫Product的setPartX()方法可以給產品物件的成員屬性設值。不同的具體建造者在實現buildPartX()方法時將有所區別,如setPartX()方法的引數可能不一樣,在有些具體建造者類中某些setPartX()方法無須實現(提供一個空實現)。而這些對於客戶端來說都無須關心,客戶端只需知道具體建造者型別即可。

在建造者模式的結構中還引入了一個指揮者類Director,該類主要有兩個作用:一方面它隔離了客戶與建立過程;另一方面它控制產品的建立過程,包括某個buildPartX()方法是否被呼叫以及多個buildPartX()方法呼叫的先後次序等。指揮者針對抽象建造者程式設計,客戶端只需要知道具體建造者的型別,即可通過指揮者類呼叫建造者的相關方法,返回一個完整的產品物件。在實際生活中也存在類似指揮者一樣的角色,如一個客戶去購買電腦,電腦銷售人員相當於指揮者,只要客戶確定電腦的型別,電腦銷售人員可以通知電腦組裝人員給客戶組裝一臺電腦。指揮者類的程式碼示例如下:

/**
 * Created by Coding小僧 on 2019/3/17
 *
 * @since 1.0
 * 指揮者類
 */
public class Director {
    private Builder builder;

    public Director(Builder builder) {
        this.builder = builder;
    }

    public void setBuilder(Builder builder) {
        this.builder = builder;
    }

    //產品構建與組裝方法
    public Product construct() {
        builder.buildPartA();
        builder.buildPartB();
        builder.buildPartC();
        return builder.getResult();
    }
}
複製程式碼

在指揮者類中可以注入一個抽象建造者型別的物件,其核心在於提供了一個建造方法construct(),在該方法中呼叫了builder物件的構造部件的方法,最後返回一個產品物件。

對於客戶端而言,只需關心具體的建造者即可,一般情況下,客戶端類程式碼片段如下所示:

……
Builder builder = new ConcreteBuilder(); //可通過配置檔案實現
Director director = new Director(builder);
Product product = director.construct();
……
複製程式碼

可以通過配置檔案來儲存具體建造者類ConcreteBuilder的類名,使得更換新的建造者時無須修改原始碼,系統擴充套件更為方便。在客戶端程式碼中,無須關心產品物件的具體組裝過程,只需指定具體建造者的型別即可。

建造者模式與抽象工廠模式有點相似,但是建造者模式返回一個完整的複雜產品,而抽象工廠模式返回一系列相關的產品;在抽象工廠模式中,客戶端通過選擇具體工廠來生成所需物件,而在建造者模式中,客戶端通過指定具體建造者型別並指導Director類如何去生成物件,側重於一步步構造一個複雜物件,然後將結果返回。如果將抽象工廠模式看成一個汽車配件生產廠,生成不同型別的汽車配件,那麼建造者模式就是一個汽車組裝廠,通過對配件進行組裝返回一輛完整的汽車。

案例回放

Sunny軟體公司遊戲開發小組決定開發一款名為《Sunny群俠傳》的網路遊戲,該遊戲採用主流的RPG(Role Playing Game,角色扮演遊戲)模式,玩家可以在遊戲中扮演虛擬世界中的一個特定角色,角色根據不同的遊戲情節和統計資料(如力量、魔法、技能等)具有不同的能力,角色也會隨著不斷升級而擁有更加強大的能力。

作為RPG遊戲的一個重要組成部分,需要對遊戲角色進行設計,而且隨著該遊戲的升級將不斷增加新的角色。不同型別的遊戲角色,其性別、臉型、服裝、髮型等外部特性都有所差異,例如“天使”擁有美麗的面容和披肩的長髮,並身穿一襲白裙;而“惡魔”極其醜陋,留著光頭並穿一件刺眼的黑衣。Sunny公司決定開發一個小工具來建立遊戲角色,可以建立不同型別的角色並可以靈活增加新的角色。

Sunny公司的開發人員通過分析發現,遊戲角色是一個複雜物件,它包含性別、臉型等多個組成部分,不同的遊戲角色其組成部分有所差異,如圖所示:

幾種不同的遊戲角色造型

無論是何種造型的遊戲角色,它的建立步驟都大同小異,都需要逐步建立其組成部分,再將各組成部分裝配成一個完整的遊戲角色。如何一步步建立一個包含多個組成部分的複雜物件,建造者模式為解決此類問題而誕生。

Sunny公司開發人員決定使用建造者模式來實現遊戲角色的建立,其基本結構如圖所示:

遊戲角色建立結構圖

在上圖中,ActorController充當指揮者,ActorBuilder充當抽象建造者,HeroBuilder、AngelBuilder和DevilBuilder充當具體建造者,Actor充當複雜產品。完整程式碼如下所示:

Actor.java

/**
 * Created by Coding小僧 on 2019/3/17
 *
 * @since 1.0
 * Actor角色類:複雜產品,考慮到程式碼的可讀性,只列出部分成員屬性,且成員屬性的型別均
 * 為String,真實情況下,有些成員屬性的型別需自定義
 */
public class Actor {
    private String type; //角色型別
    private String sex; //性別

    private String face; //臉型
    private String costume; //服裝
    private String hairstyle; //髮型

    public void setType(String type) {
        this.type = type;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public void setFace(String face) {
        this.face = face;
    }

    public void setCostume(String costume) {
        this.costume = costume;
    }

    public void setHairstyle(String hairstyle) {
        this.hairstyle = hairstyle;
    }

    public String getType() {
        return (this.type);
    }

    public String getSex() {
        return (this.sex);
    }

    public String getFace() {
        return (this.face);
    }

    public String getCostume() {
        return (this.costume);
    }

    public String getHairstyle() {
        return (this.hairstyle);
    }
}
複製程式碼

HeroBuilder.java

/**
 * Created by Coding小僧 on 2019/3/17
 *
 * @since 1.0
 * 英雄角色建造器:具體建造者
 */
public class HeroBuilder extends ActorBuilder {
    public void buildType() {
        actor.setType("英雄");
    }

    public void buildSex() {
        actor.setSex("男");
    }

    public void buildFace() {
        actor.setFace("英俊");
    }

    public void buildCostume() {
        actor.setCostume("盔甲");
    }

    public void buildHairstyle() {
        actor.setHairstyle("飄逸");
    }
}
複製程式碼

AngelBuilder.java

/**
 * Created by Coding小僧 on 2019/3/17
 *
 * @since 1.0
 * 天使角色建造器:具體建造者
 */
public class AngelBuilder extends ActorBuilder {
    public void buildType() {
        actor.setType("天使");
    }

    public void buildSex() {
        actor.setSex("女");
    }

    public void buildFace() {
        actor.setFace("漂亮");
    }

    public void buildCostume() {
        actor.setCostume("白裙");
    }

    public void buildHairstyle() {
        actor.setHairstyle("披肩長髮");
    }
}
複製程式碼

DevilBuilder.java

/**
 * Created by Coding小僧 on 2019/3/17
 *
 * @since 1.0
 * 惡魔角色建造器:具體建造者
 */
public class DevilBuilder extends ActorBuilder {
    public void buildType() {
        actor.setType("惡魔");
    }

    public void buildSex() {
        actor.setSex("妖");
    }

    public void buildFace() {
        actor.setFace("醜陋");
    }

    public void buildCostume() {
        actor.setCostume("黑衣");
    }

    public void buildHairstyle() {
        actor.setHairstyle("光頭");
    }
}
複製程式碼

指揮者類ActorController定義了construct()方法,該方法擁有一個抽象建造者ActorBuilder型別的引數,在該方法內部實現了遊戲角色物件的逐步構建,程式碼如下所示:

ActorController.java

/**
 * Created by Coding小僧 on 2019/3/17
 *
 * @since 1.0
 * 遊戲角色建立控制器:指揮者
 */
public class ActorController {
    //逐步構建複雜產品物件
    public Actor construct(ActorBuilder ab) {
        Actor actor;
        ab.buildType();
        ab.buildSex();
        ab.buildFace();
        ab.buildCostume();
        ab.buildHairstyle();
        actor = ab.createActor();
        return actor;
    }
}
複製程式碼

編寫如下客戶端測試程式碼:

Client.java

/**
 * Created by Coding小僧 on 2019/3/17
 *
 * @since 1.0
 * 客戶端測試程式碼
 */
public class Client {
    public static void main(String args[]) {
        //針對抽象建造者程式設計
        ActorBuilder angelBuilder = new AngelBuilder();
        // ActorBuilder heroBuilder = new HeroBuilder();
        // ActorBuilder devilBuilder = new DevilBuilder();
        
        ActorController actorController = new ActorController();

        //通過指揮者建立完整的建造者物件
        Actor actor = actorController.construct(angelBuilder);
        System.out.println(actor.getType() + "的外觀:");
        System.out.println("性別:" + actor.getSex());
        System.out.println("面容:" + actor.getFace());
        System.out.println("服裝:" + actor.getCostume());
        System.out.println("髮型:" + actor.getHairstyle());
    }
}
複製程式碼

編譯並執行程式,輸出結果如下:

天使的外觀:
性別:女
面容:漂亮
服裝:白裙
髮型:披肩長髮
複製程式碼

在建造者模式中,客戶端只需例項化指揮者類,指揮者類針對抽象建造者程式設計,客戶端根據需要傳入具體的建造者型別,指揮者將指導具體建造者一步一步構造一個完整的產品(逐步呼叫具體建造者的buildX()方法),相同的構造過程可以建立完全不同的產品。在遊戲角色例項中,如果需要更換角色,只需要修改配置檔案,更換具體角色建造者類即可;如果需要增加新角色,可以增加一個新的具體角色建造者類作為抽象角色建造者的子類,再修改配置檔案即可,原有程式碼無須修改,完全符合“開閉原則”。

典型應用

Android中廣泛使用的AlertDialog彈窗是建造者模式的應用之一,這裡為了節省篇幅,就不在詳細分析AlertDialog在原始碼中的詳細呼叫流程。

AlertDialog時序圖

優點

1.在建造者模式中,客戶端不必知道產品內部組成的細節,將產品本身與產品的建立過程解耦,使得相同的建立過程可以建立不同的產品物件。

2.每一個具體建造者都相對獨立,而與其他的具體建造者無關,因此可以很方便地替換具體建造者或增加新的具體建造者,使用者使用不同的具體建造者即可得到不同的產品物件。由於指揮者類針對抽象建造者程式設計,增加新的具體建造者無須修改原有類庫的程式碼,系統擴充套件方便,符合“開閉原則”。

3.可以更加精細地控制產品的建立過程。將複雜產品的建立步驟分解在不同的方法中,使得建立過程更加清晰,也更方便使用程式來控制建立過程。

缺點

1.建造者模式所建立的產品一般具有較多的共同點,其組成部分相似,如果產品之間的差異性很大,例如很多組成部分都不相同,不適合使用建造者模式,因此其使用範圍受到一定的限制。

2.如果產品的內部變化複雜,可能會導致需要定義很多具體建造者類來實現這種變化,導致系統變得很龐大,增加系統的理解難度和執行成本。

重點

建造者模式的核心在於如何一步步構建一個包含多個組成部件的完整物件,使用相同的構建過程構建不同的產品,在軟體開發中,如果我們需要建立複雜物件並希望系統具備很好的靈活性和可擴充套件性可以考慮使用建造者模式。

使用場景

在以下情況下可以考慮使用建造者模式:

1.需要生成的產品物件有複雜的內部結構,這些產品物件通常包含多個成員屬性。

2.需要生成的產品物件的屬性相互依賴,需要指定其生成順序。

3.物件的建立過程獨立於建立該物件的類。在建造者模式中通過引入了指揮者類,將建立過程封裝在指揮者類中,而不在建造者類和客戶類中。

4.隔離複雜物件的建立和使用,並使得相同的建立過程可以建立不同的產品。

關注我的技術公眾號"程式設計師驛站",不更新技術文章,微信掃一掃下方二維碼即可關注:

重識設計模式-建造者模式(Builder Pattern)

相關文章