創建型SEQ6 - 建造者模式 Builder Pattern

【學習難度:★★★★☆,使用頻率:★★☆☆☆】
直接出處:建造者模式
梳理和學習:https://github.com/BruceOuyang/boy-design-pattern
簡書日期: 2018/03/08
簡書首頁:http://www.lxweimin.com/p/0fb891a7c5ed

復雜對象的組裝與創建——建造者模式(一)

沒有人買車會只買一個輪胎或者方向盤,大家買的都是一輛包含輪胎、方向盤和發動機等多個部件的完整汽車。如何將這些部件組裝成一輛完整的汽車并返回給用戶,這是建造者模式需要解決的問題。建造者模式又稱為生成器模式,它是一種較為復雜、使用頻率也相對較低的創建型模式。建造者模式為客戶端返回的不是一個簡單的產品,而是一個由多個部件組成的復雜產品。

8.1 游戲角色設計

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

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

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

圖8-1 幾種不同的游戲角色造型(注:本圖中的游戲角色造型來源于網絡,特此說明)

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

8.2 建造者模式概述

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

建造者模式定義如下:

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

建造者模式一步一步創建一個復雜的對象,它允許用戶只通過指定復雜對象的類型和內容就可以構建它們,用戶不需要知道內部的具體構建細節。建造者模式結構如圖8-2所示:

圖8-2 建造者模式結構圖

在建造者模式結構圖中包含如下幾個角色:

  • Builder(抽象建造者):它為創建一個產品Product對象的各個部件指定抽象接口,在該接口中一般聲明兩類方法,一類方法是buildPartX(),它們用于創建復雜對象的各個部件;另一類方法是getResult(),它們用于返回復雜對象。Builder既可以是抽象類,也可以是接口。

  • ConcreteBuilder(具體建造者):它實現了Builder接口,實現各個部件的具體構造和裝配方法,定義并明確它所創建的復雜對象,也可以提供一個方法返回創建好的復雜產品對象。

  • Product(產品角色):它是被構建的復雜對象,包含多個組成部件,具體建造者創建該產品的內部表示并定義它的裝配過程。

  • Director(指揮者):指揮者又稱為導演類,它負責安排復雜對象的建造次序,指揮者與抽象建造者之間存在關聯關系,可以在其construct()建造方法中調用建造者對象的部件構造與裝配方法,完成復雜對象的建造。客戶端一般只需要與指揮者進行交互,在客戶端確定具體建造者的類型,并實例化具體建造者對象(也可以通過配置文件和反射機制),然后通過指揮者類的構造函數或者Setter方法將該對象傳入指揮者類中。

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

class Product  {
    //定義部件,部件可以是任意類型,包括值類型和引用類型
    private  String partA; 
    private  String partB;
    private  String partC;
    //partA的Getter方法和Setter方法省略
    //partB的Getter方法和Setter方法省略
    //partC的Getter方法和Setter方法省略
}

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

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()方法調用的先后次序等。指揮者針對抽象建造者編程,客戶端只需要知道具體建造者的類型,即可通過指揮者類調用建造者的相關方法,返回一個完整的產品對象。在實際生活中也存在類似指揮者一樣的角色,如一個客戶去購買電腦,電腦銷售人員相當于指揮者,只要客戶確定電腦的類型,電腦銷售人員可以通知電腦組裝人員給客戶組裝一臺電腦。指揮者類的代碼示例如下:

class Director {
    private  Builder builder;

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

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

    //產品構建與組裝方法
    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類如何去生成對象,側重于一步步構造一個復雜對象,然后將結果返回。如果將抽象工廠模式看成一個汽車配件生產廠,生成不同類型的汽車配件,那么建造者模式就是一個汽車組裝廠,通過對配件進行組裝返回一輛完整的汽車。

思考

如果沒有指揮者類Director,客戶端將如何構建復雜產品?

復雜對象的組裝與創建——建造者模式(二)

8.3 完整解決方案

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

圖8-3 游戲角色創建結構圖

在圖8-3中,ActorController充當指揮者,ActorBuilder充當抽象建造者,HeroBuilder、AngelBuilder和DevilBuilder充當具體建造者,Actor充當復雜產品。完整代碼如下所示: //Actor角色類:復雜產品,考慮到代碼的可讀性,只列出部分成員屬性,且成員屬性的類型均為String,真實情況下,有些成員屬性的類型需自定義

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

//角色建造器:抽象建造者
abstract class ActorBuilder
{
       protected  Actor actor = new Actor();

       public  abstract void buildType();
       public  abstract void buildSex();
       public  abstract void buildFace();
       public  abstract void buildCostume();
       public  abstract void buildHairstyle();

    //工廠方法,返回一個完整的游戲角色對象
       public Actor createActor()
       {
              return actor;
       }
}

//英雄角色建造器:具體建造者
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("飄逸");
       }    
}

//天使角色建造器:具體建造者
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("披肩長發");
       }    
}

//惡魔角色建造器:具體建造者
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類型的參數,在該方法內部實現了游戲角色對象的逐步構建,代碼如下所示:

//游戲角色創建控制器:指揮者
class ActorController
{
    //逐步構建復雜產品對象
       public Actor construct(ActorBuilder ab)
       {
              Actor actor;
              ab.buildType();
              ab.buildSex();
              ab.buildFace();
              ab.buildCostume();
              ab.buildHairstyle();
              actor=ab.createActor();
              return actor;
       }
}

為了提高系統的靈活性和可擴展性,我們將具體建造者類的類名存儲在配置文件中,并通過工具類XMLUtil來讀取配置文件并反射生成對象,XMLUtil類的代碼如下所示:

import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import java.io.*;
class XMLUtil
{
//該方法用于從XML配置文件中提取具體類類名,并返回一個實例對象
       public  static Object getBean()
       {
              try
              {
                     //創建文檔對象
                     DocumentBuilderFactory  dFactory = DocumentBuilderFactory.newInstance();
                     DocumentBuilder  builder = dFactory.newDocumentBuilder();
                     Document  doc;                                                
                     doc  = builder.parse(new File("config.xml"));

                     //獲取包含類名的文本節點
                     NodeList  nl = doc.getElementsByTagName("className");
            Node  classNode=nl.item(0).getFirstChild();
            String  cName=classNode.getNodeValue();

            //通過類名生成實例對象并將其返回
            Class c=Class.forName(cName);
                 Object obj=c.newInstance();
            return obj;
         }  
         catch(Exception e)
         {
              e.printStackTrace();
              return null;
          }
       }
}

配置文件config.xml中存儲了具體建造者類的類名,代碼如下所示:

<?xml version="1.0"?>
<config>
       <className>AngelBuilder</className>
</config>   

編寫如下客戶端測試代碼:

class Client
{
       public  static void main(String args[])
       {
            ActorBuilder ab; //針對抽象建造者編程
            ab =  (ActorBuilder)XMLUtil.getBean(); //反射生成具體建造者對象

            ActorController ac = new  ActorController();
            Actor actor;
            actor = ac.construct(ab); //通過指揮者創建完整的建造者對象

            String  type = actor.getType();
            System.out.println(type  + "的外觀:");
            System.out.println("性別:" + actor.getSex());
            System.out.println("面容:" + actor.getFace());
            System.out.println("服裝:" + actor.getCostume());
            System.out.println("發型:" + actor.getHairstyle());
       }
}

編譯并運行程序,輸出結果如下:

天使的外觀:
性別:女
面容:漂亮
服裝:白裙
發型:披肩長發

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

復雜對象的組裝與創建——建造者模式(三)

8.4 關于Director的進一步討論

指揮者類Director在建造者模式中扮演非常重要的作用,簡單的Director類用于指導具體建造者如何構建產品,它按一定次序調用Builder的buildPartX()方法,控制調用的先后次序,并向客戶端返回一個完整的產品對象。下面我們討論幾種Director的高級應用方式:

1.省略Director

在有些情況下,為了簡化系統結構,可以將Director和抽象建造者Builder進行合并,在Builder中提供逐步構建復雜產品對象的construct()方法。由于Builder類通常為抽象類,因此可以將construct()方法定義為靜態(static)方法。如果將游戲角色設計中的指揮者類ActorController省略,ActorBuilder類的代碼修改如下:

abstract class ActorBuilder
{
       protected static Actor actor = new  Actor();

       public  abstract void buildType();
       public  abstract void buildSex();
       public  abstract void buildFace();
       public  abstract void buildCostume();
       public  abstract void buildHairstyle();

       public static Actor  construct(ActorBuilder ab)
       {
              ab.buildType();
              ab.buildSex();
              ab.buildFace();
              ab.buildCostume();
              ab.buildHairstyle();
              return actor;
       }
}

對應的客戶端代碼也將發生修改,其代碼片段如下所示:

        ……
        ActorBuilder  ab;
        ab  = (ActorBuilder)XMLUtil.getBean();

        Actor  actor;
        actor =  ActorBuilder.construct(ab);
        ……

除此之外,還有一種更簡單的處理方法,可以將construct()方法的參數去掉,直接在construct()方法中調用buildPartX()方法,代碼如下所示:

abstract class ActorBuilder
{
       protected  Actor actor = new Actor();

       public  abstract void buildType();
       public  abstract void buildSex();
       public  abstract void buildFace();
       public  abstract void buildCostume();
       public  abstract void buildHairstyle();

       public Actor construct()
       {
              this.buildType();
              this.buildSex();
              this.buildFace();
              this.buildCostume();
              this.buildHairstyle();
              return actor;
       }
}

客戶端代碼代碼片段如下所示:

        ……
        ActorBuilder  ab;
        ab  = (ActorBuilder)XMLUtil.getBean();
        
        Actor  actor;
        actor = ab.construct();
        ……

此時,construct()方法定義了其他buildPartX()方法調用的次序,為其他方法的執行提供了一個流程模板,這與我們在后面要學習的模板方法模式非常類似。

以上兩種對Director類的省略方式都不影響系統的靈活性和可擴展性,同時還簡化了系統結構,但加重了抽象建造者類的職責,如果construct()方法較為復雜,待構建產品的組成部分較多,建議還是將construct()方法單獨封裝在Director中,這樣做更符合“單一職責原則”。

2.鉤子方法的引入

建造者模式除了逐步構建一個復雜產品對象外,還可以通過Director類來更加精細地控制產品的創建過程,例如增加一類稱之為鉤子方法(HookMethod)的特殊方法來控制是否對某個buildPartX()的調用。

鉤子方法的返回類型通常為boolean類型,方法名一般為isXXX(),鉤子方法定義在抽象建造者類中。例如我們可以在游戲角色的抽象建造者類ActorBuilder中定義一個方法isBareheaded(),用于判斷某個角色是否為“光頭(Bareheaded)”,在ActorBuilder為之提供一個默認實現,其返回值為false,代碼如下所示:

abstract class ActorBuilder
{
       protected  Actor actor = new Actor();

       public  abstract void buildType();
       public  abstract void buildSex();
       public  abstract void buildFace();
       public  abstract void buildCostume();
       public  abstract void buildHairstyle();

       //鉤子方法
       public boolean isBareheaded()
       {
              return false;
       }

       public  Actor createActor()
       {
              return  actor;
       }
}

如果某個角色無須構建頭發部件,例如“惡魔(Devil)”,則對應的具體建造器DevilBuilder將覆蓋isBareheaded()方法,并將返回值改為true,代碼如下所示:

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("光頭");
       }
       //覆蓋鉤子方法
       public boolean isBareheaded()
       {
              return true;
       }     
}

此時,指揮者類ActorController的代碼修改如下:

class ActorController
{
       public  Actor construct(ActorBuilder ab)
       {
              Actor  actor;
              ab.buildType();
              ab.buildSex();
              ab.buildFace();
              ab.buildCostume();
              //通過鉤子方法來控制產品的構建
              if(!ab.isBareheaded())
              {
                     ab. buildHairstyle();
              }
              actor=ab.createActor();
              return  actor;
       }
}

當在客戶端代碼中指定具體建造者類型并通過指揮者來實現產品的逐步構建時,將調用鉤子方法isBareheaded()來判斷游戲角色是否有頭發,如果isBareheaded()方法返回true,即沒有頭發,則跳過構建發型的方法buildHairstyle();否則將執行buildHairstyle()方法。通過引入鉤子方法,我們可以在Director中對復雜產品的構建進行精細的控制,不僅指定buildPartX()方法的執行順序,還可以控制是否需要執行某個buildPartX()方法。

8.5 建造者模式總結

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

  1. 主要優點

建造者模式的主要優點如下:

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

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

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

  1. 主要缺點

建造者模式的主要缺點如下:

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

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

  1. 適用場景

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

(1) 需要生成的產品對象有復雜的內部結構,這些產品對象通常包含多個成員屬性。

(2) 需要生成的產品對象的屬性相互依賴,需要指定其生成順序。

(3) 對象的創建過程獨立于創建該對象的類。在建造者模式中通過引入了指揮者類,將創建過程封裝在指揮者類中,而不在建造者類和客戶類中。

(4) 隔離復雜對象的創建和使用,并使得相同的創建過程可以創建不同的產品。

練習

Sunny軟件公司欲開發一個視頻播放軟件,為了給用戶使用提供方便,該播放軟件提供多種界面顯示模式,如完整模式、精簡模式、記憶模式、網絡模式等。在不同的顯示模式下主界面的組成元素有所差異,如在完整模式下將顯示菜單、播放列表、主窗口、控制條等,在精簡模式下只顯示主窗口和控制條,而在記憶模式下將顯示主窗口、控制條、收藏列表等。嘗試使用建造者模式設計該軟件。

練習會在我的github上做掉

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。