03、建造者模式

建造者模式的原理和代碼實(shí)現(xiàn)非常簡(jiǎn)單,掌握起來(lái)并不難,難點(diǎn)在于應(yīng)用場(chǎng)景。

比如,你有沒(méi)有考慮過(guò)這樣幾個(gè)問(wèn)題:直接使用構(gòu)造函數(shù)或者配合 set 方法就能創(chuàng)建對(duì)象,為什么還需要建造者模式來(lái)創(chuàng)建呢?建造者模式和工廠模式都可以創(chuàng)建對(duì)象,那它們兩個(gè)的區(qū)別在哪里呢?

為什么需要建造者模式?

在平時(shí)的開(kāi)發(fā)中,創(chuàng)建一個(gè)對(duì)象最常用的方式是,使用 new 關(guān)鍵字調(diào)用類(lèi)的構(gòu)造函數(shù)來(lái)完成。我的問(wèn)題是,什么情況下這種方式就不適用了,就需要采用建造者模式來(lái)創(chuàng)建對(duì)象呢?你可以先思考一下,下面我通過(guò)一個(gè)例子來(lái)帶你看一下。

假設(shè)有這樣一道設(shè)計(jì)面試題:我們需要定義一個(gè)資源池配置類(lèi) ResourcePoolConfig。這里的資源池,你可以簡(jiǎn)單理解為線(xiàn)程池、連接池、對(duì)象池等。在這個(gè)資源池配置類(lèi)中,有以下幾個(gè)成員變量,也就是可配置項(xiàng)。現(xiàn)在,請(qǐng)你編寫(xiě)代碼實(shí)現(xiàn)這個(gè) ResourcePoolConfig類(lèi)。

成員變量 解釋 是否必填 默認(rèn)值
name 資源名稱(chēng) 沒(méi)有
maxTotal 最大總資源數(shù)量 8
maxIdle 最大空閑資源數(shù) 8
minIdel 最小空閑資源數(shù)量 0

只要你稍微有點(diǎn)開(kāi)發(fā)經(jīng)驗(yàn),那實(shí)現(xiàn)這樣一個(gè)類(lèi)對(duì)你來(lái)說(shuō)并不是件難事。最常見(jiàn)、最容易想到的實(shí)現(xiàn)思路如下代碼所示。因?yàn)?maxTotal、maxIdle、minIdle 不是必填變量,所以在創(chuàng)建 ResourcePoolConfig 對(duì)象的時(shí)候,我們通過(guò)往構(gòu)造函數(shù)中,給這幾個(gè)參數(shù)傳遞 null值,來(lái)表示使用默認(rèn)值。

public class ResourcePoolConfig {
   private static final int DEFAULT_MAX_TOTAL = 8;
   private static final int DEFAULT_MAX_IDLE = 8;
   private static final int DEFAULT_MIN_IDLE = 0;
   private String name;
   private int maxTotal = DEFAULT_MAX_TOTAL;
   private int maxIdle = DEFAULT_MAX_IDLE;
   private int minIdle = DEFAULT_MIN_IDLE;

   public ResourcePoolConfig(String name, Integer maxTotal, Integer maxIdle, Int minIdle){
       if (StringUtils.isBlank(name)) {
         throw new IllegalArgumentException("name should not be empty.");
       }

       this.name = name;

       if (maxTotal != null) {
           if (maxTotal <= 0) {
               throw new IllegalArgumentException("maxTotal should be positive.");
           }
           this.maxTotal = maxTotal;
       }

       if (maxIdle != null) {
           if (maxIdle < 0) {
               throw new IllegalArgumentException("maxIdle should not be negative.");
           }
           this.maxIdle = maxIdle;
       }

       if (minIdle != null) {
           if (minIdle < 0) {
               throw new IllegalArgumentException("minIdle should not be negative.");
           }
           this.minIdle = minIdle;
       }
   }
   //...省略getter方法...
}

現(xiàn)在,ResourcePoolConfig 只有 4 個(gè)可配置項(xiàng),對(duì)應(yīng)到構(gòu)造函數(shù)中,也只有 4 個(gè)參數(shù),參數(shù)的個(gè)數(shù)不多。但是,如果可配置項(xiàng)逐漸增多,變成了 8 個(gè)、10 個(gè),甚至更多,那繼續(xù)沿用現(xiàn)在的設(shè)計(jì)思路,構(gòu)造函數(shù)的參數(shù)列表會(huì)變得很長(zhǎng),代碼在可讀性和易用性上都會(huì)變差。在使用構(gòu)造函數(shù)的時(shí)候,我們就容易搞錯(cuò)各參數(shù)的順序,傳遞進(jìn)錯(cuò)誤的參數(shù)值,導(dǎo)致非常隱蔽的 bug。

// 參數(shù)太多,導(dǎo)致可讀性差、參數(shù)可能傳遞錯(cuò)誤
ResourcePoolConfig config = new ResourcePoolConfig("dbconnectionpool", 16, null, null)

解決這個(gè)問(wèn)題的辦法你應(yīng)該也已經(jīng)想到了,那就是用 set() 函數(shù)來(lái)給成員變量賦值,以替代冗長(zhǎng)的構(gòu)造函數(shù)。我們直接看代碼,具體如下所示。其中,配置項(xiàng) name 是必填的,所以我們把它放到構(gòu)造函數(shù)中設(shè)置,強(qiáng)制創(chuàng)建類(lèi)對(duì)象的時(shí)候就要填寫(xiě)。其他配置項(xiàng) maxTotal、maxIdle、minIdle 都不是必填的,所以我們通過(guò) set() 函數(shù)來(lái)設(shè)置,讓使用者自主選擇填寫(xiě)或者不填寫(xiě)。

public class ResourcePoolConfig {
    private static final int DEFAULT_MAX_TOTAL = 8;
    private static final int DEFAULT_MAX_IDLE = 8;
    private static final int DEFAULT_MIN_IDLE = 0;
    private String name;
    private int maxTotal = DEFAULT_MAX_TOTAL;
    private int maxIdle = DEFAULT_MAX_IDLE;
    private int minIdle = DEFAULT_MIN_IDLE;

    public ResourcePoolConfig(String name) {
        if (StringUtils.isBlank(name)) {
            throw new IllegalArgumentException("name should not be empty.");
        }
        this.name = name;
    }

    public void setMaxTotal(int maxTotal) {
        if (maxTotal <= 0) {
            throw new IllegalArgumentException("maxTotal should be positive.");
        }
        this.maxTotal = maxTotal;
    }

    public void setMaxIdle(int maxIdle) {
        if (maxIdle < 0) {
            throw new IllegalArgumentException("maxIdle should not be negative.");
        }
        this.maxIdle = maxIdle;
    }

    public void setMinIdle(int minIdle) {
        if (minIdle < 0) {
            throw new IllegalArgumentException("minIdle should not be negative.");
        }
        this.minIdle = minIdle;
    }

    //...省略getter方法...
}

接下來(lái),我們來(lái)看新的 ResourcePoolConfig 類(lèi)該如何使用。我寫(xiě)了一個(gè)示例代碼,如下
所示。沒(méi)有了冗長(zhǎng)的函數(shù)調(diào)用和參數(shù)列表,代碼在可讀性和易用性上提高了很多。

// ResourcePoolConfig使用舉例
ResourcePoolConfig config = new ResourcePoolConfig("dbconnectionpool");
config.setMaxTotal(16);
config.setMaxIdle(8);

至此,我們?nèi)匀粵](méi)有用到建造者模式,通過(guò)構(gòu)造函數(shù)設(shè)置必填項(xiàng),通過(guò) set() 方法設(shè)置可選配置項(xiàng),就能實(shí)現(xiàn)我們的設(shè)計(jì)需求。如果我們把問(wèn)題的難度再加大點(diǎn),比如,還需要解決下面這三個(gè)問(wèn)題,那現(xiàn)在的設(shè)計(jì)思路就不能滿(mǎn)足了。

  • 我們剛剛講到,name 是必填的,所以,我們把它放到構(gòu)造函數(shù)中,強(qiáng)制創(chuàng)建對(duì)象的時(shí)候就設(shè)置。如果必填的配置項(xiàng)有很多,把這些必填配置項(xiàng)都放到構(gòu)造函數(shù)中設(shè)置,那構(gòu)造函數(shù)就又會(huì)出現(xiàn)參數(shù)列表很長(zhǎng)的問(wèn)題。如果我們把必填項(xiàng)也通過(guò) set() 方法設(shè)置,那校驗(yàn)這些必填項(xiàng)是否已經(jīng)填寫(xiě)的邏輯就無(wú)處安放了。
  • 除此之外,假設(shè)配置項(xiàng)之間有一定的依賴(lài)關(guān)系,比如,如果用戶(hù)設(shè)置了 maxTotal、maxIdle、minIdle 其中一個(gè),就必須顯式地設(shè)置另外兩個(gè);或者配置項(xiàng)之間有一定的約束條件,比如,maxIdle 和 minIdle 要小于等于 maxTotal。如果我們繼續(xù)使用現(xiàn)在的設(shè)計(jì)思路,那這些配置項(xiàng)之間的依賴(lài)關(guān)系或者約束條件的校驗(yàn)邏輯就無(wú)處安放了。
  • 如果我們希望 ResourcePoolConfig 類(lèi)對(duì)象是不可變對(duì)象,也就是說(shuō),對(duì)象在創(chuàng)建好之后,就不能再修改內(nèi)部的屬性值。要實(shí)現(xiàn)這個(gè)功能,我們就不能在ResourcePoolConfig 類(lèi)中暴露 set() 方法。

為了解決以上三個(gè)問(wèn)題,建造者模式就派上用場(chǎng)了。

我們可以把校驗(yàn)邏輯放置到 Builder 類(lèi)中,先創(chuàng)建建造者,并且通過(guò) set() 方法設(shè)置建造者的變量值,然后在使用 build() 方法真正創(chuàng)建對(duì)象之前,做集中的校驗(yàn),校驗(yàn)通過(guò)之后才會(huì)創(chuàng)建對(duì)象。除此之外,我們把 ResourcePoolConfig 的構(gòu)造函數(shù)改為 private 私有權(quán)限。這樣我們就只能通過(guò)建造者來(lái)創(chuàng)建 ResourcePoolConfig 類(lèi)對(duì)象。并且,ResourcePoolConfig 沒(méi)有提供任何 set() 方法,這樣我們創(chuàng)建出來(lái)的對(duì)象就是不可變對(duì)象了。

我們用建造者模式重新實(shí)現(xiàn)了上面的需求,具體的代碼如下所示:

public class ResourcePoolConfig {
    private String name;
    private int maxTotal;
    private int maxIdle;
    private int minIdle;

    private ResourcePoolConfig(Builder builder) {
        this.name = builder.name;
        this.maxTotal = builder.maxTotal;
        this.maxIdle = builder.maxIdle;
        this.minIdle = builder.minIdle;
    }

    //...省略getter方法...

    //我們將Builder類(lèi)設(shè)計(jì)成了ResourcePoolConfig的內(nèi)部類(lèi)。
    //我們也可以將Builder類(lèi)設(shè)計(jì)成獨(dú)立的非內(nèi)部類(lèi)ResourcePoolConfigBuilder。
    public static class Builder {
        private static final int DEFAULT_MAX_TOTAL = 8;
        private static final int DEFAULT_MAX_IDLE = 8;
        private static final int DEFAULT_MIN_IDLE = 0;
        private String name;
        private int maxTotal = DEFAULT_MAX_TOTAL;
        private int maxIdle = DEFAULT_MAX_IDLE;
        private int minIdle = DEFAULT_MIN_IDLE;

        public ResourcePoolConfig build() {
            // 校驗(yàn)邏輯放到這里來(lái)做,包括必填項(xiàng)校驗(yàn)、依賴(lài)關(guān)系校驗(yàn)、約束條件校驗(yàn)等
            if (StringUtils.isBlank(name)) {
                throw new IllegalArgumentException("...");
            }
            if (maxIdle > maxTotal) {
                throw new IllegalArgumentException("...");
            }
            if (minIdle > maxTotal || minIdle > maxIdle) {
                throw new IllegalArgumentException("...");
            }
            return new ResourcePoolConfig(this);
        }

        public Builder setName(String name) {
            if (StringUtils.isBlank(name)) {
                throw new IllegalArgumentException("...");
            }
            this.name = name;
            return this;
        }

        public Builder setMaxTotal(int maxTotal) {
            if (maxTotal <= 0) {
                throw new IllegalArgumentException("...");
            }
            this.maxTotal = maxTotal;
            return this;
        }

        public Builder setMaxIdle(int maxIdle) {
            if (maxIdle < 0) {
                throw new IllegalArgumentException("...");
            }
            this.maxIdle = maxIdle;
            return this;
        }

        public Builder setMinIdle(int minIdle) {
            if (minIdle < 0) {
                throw new IllegalArgumentException("...");
            }
            this.minIdle = minIdle;
            return this;
        }
    }
}

// 這段代碼會(huì)拋出IllegalArgumentException,因?yàn)閙inIdle>maxIdle
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
.setName("dbconnectionpool")
.setMaxTotal(16)
.setMaxIdle(10)
.setMinIdle(12)
.build();

實(shí)際上,使用建造者模式創(chuàng)建對(duì)象,還能避免對(duì)象存在無(wú)效狀態(tài)。我再舉個(gè)例子解釋一下。比如我們定義了一個(gè)長(zhǎng)方形類(lèi),如果不使用建造者模式,采用先創(chuàng)建后 set 的方式,那就會(huì)導(dǎo)致在第一個(gè) set 之后,對(duì)象處于無(wú)效狀態(tài)。具體代碼如下所示:

Rectangle r = new Rectange(); // r is invalid
r.setWidth(2); // r is invalid
r.setHeight(3); // r is valid

為了避免這種無(wú)效狀態(tài)的存在,我們就需要使用構(gòu)造函數(shù)一次性初始化好所有的成員變量。如果構(gòu)造函數(shù)參數(shù)過(guò)多,我們就需要考慮使用建造者模式,先設(shè)置建造者的變量,然后再一次性地創(chuàng)建對(duì)象,讓對(duì)象一直處于有效狀態(tài)。

實(shí)際上,如果我們并不是很關(guān)心對(duì)象是否有短暫的無(wú)效狀態(tài),也不是太在意對(duì)象是否是可變的。比如,對(duì)象只是用來(lái)映射數(shù)據(jù)庫(kù)讀出來(lái)的數(shù)據(jù),那我們直接暴露 set() 方法來(lái)設(shè)置類(lèi)的成員變量值是完全沒(méi)問(wèn)題的。而且,使用建造者模式來(lái)構(gòu)建對(duì)象,代碼實(shí)際上是有點(diǎn)重復(fù)的,ResourcePoolConfig 類(lèi)中的成員變量,要在 Builder 類(lèi)中重新再定義一遍。

與工廠模式有何區(qū)別?

從上面的講解中,我們可以看出,建造者模式是讓建造者類(lèi)來(lái)負(fù)責(zé)對(duì)象的創(chuàng)建工作。上一節(jié)課中講到的工廠模式,是由工廠類(lèi)來(lái)負(fù)責(zé)對(duì)象創(chuàng)建的工作。那它們之間有什么區(qū)別呢?

實(shí)際上,工廠模式是用來(lái)創(chuàng)建不同但是相關(guān)類(lèi)型的對(duì)象(繼承同一父類(lèi)或者接口的一組子類(lèi)),由給定的參數(shù)來(lái)決定創(chuàng)建哪種類(lèi)型的對(duì)象。建造者模式是用來(lái)創(chuàng)建一種類(lèi)型的復(fù)雜對(duì)象,通過(guò)設(shè)置不同的可選參數(shù),“定制化”地創(chuàng)建不同的對(duì)象。

網(wǎng)上有一個(gè)經(jīng)典的例子很好地解釋了兩者的區(qū)別。
顧客走進(jìn)一家餐館點(diǎn)餐,我們利用工廠模式,根據(jù)用戶(hù)不同的選擇,來(lái)制作不同的食物,比如披薩、漢堡、沙拉。對(duì)于披薩來(lái)說(shuō),用戶(hù)又有各種配料可以定制,比如奶酪、西紅柿、起司,我們通過(guò)建造者模式根據(jù)用戶(hù)選擇的不同配料來(lái)制作披薩。

實(shí)際上,我們也不要太學(xué)院派,非得把工廠模式、建造者模式分得那么清楚,我們需要知道的是,每個(gè)模式為什么這么設(shè)計(jì),能解決什么問(wèn)題。只有了解了這些最本質(zhì)的東西,我們才能不生搬硬套,才能靈活應(yīng)用,甚至可以混用各種模式創(chuàng)造出新的模式,來(lái)解決特定場(chǎng)景的問(wèn)題。

總結(jié)

好了,內(nèi)容到此就講完了。我們一塊來(lái)總結(jié)回顧一下,你需要重點(diǎn)掌握的內(nèi)容。建造者模式的原理和實(shí)現(xiàn)比較簡(jiǎn)單,重點(diǎn)是掌握應(yīng)用場(chǎng)景,避免過(guò)度使用。

如果一個(gè)類(lèi)中有很多屬性,為了避免構(gòu)造函數(shù)的參數(shù)列表過(guò)長(zhǎng),影響代碼的可讀性和易用性,我們可以通過(guò)構(gòu)造函數(shù)配合 set() 方法來(lái)解決。但是,如果存在下面情況中的任意一種,我們就要考慮使用建造者模式了。

  • 我們把類(lèi)的必填屬性放到構(gòu)造函數(shù)中,強(qiáng)制創(chuàng)建對(duì)象的時(shí)候就設(shè)置。如果必填的屬性有很多,把這些必填屬性都放到構(gòu)造函數(shù)中設(shè)置,那構(gòu)造函數(shù)就又會(huì)出現(xiàn)參數(shù)列表很長(zhǎng)的問(wèn)題。如果我們把必填屬性通過(guò) set() 方法設(shè)置,那校驗(yàn)這些必填屬性是否已經(jīng)填寫(xiě)的邏輯就無(wú)處安放了。
  • 如果類(lèi)的屬性之間有一定的依賴(lài)關(guān)系或者約束條件,我們繼續(xù)使用構(gòu)造函數(shù)配合 set() 方法的設(shè)計(jì)思路,那這些依賴(lài)關(guān)系或約束條件的校驗(yàn)邏輯就無(wú)處安放了。
  • 如果我們希望創(chuàng)建不可變對(duì)象,也就是說(shuō),對(duì)象在創(chuàng)建好之后,就不能再修改內(nèi)部的屬性值,要實(shí)現(xiàn)這個(gè)功能,我們就不能在類(lèi)中暴露 set() 方法。構(gòu)造函數(shù)配合 set() 方法來(lái)設(shè)置屬性值的方式就不適用了。

除此之外,文中我們還對(duì)比了工廠模式和建造者模式的區(qū)別。工廠模式是用來(lái)創(chuàng)建不同但是相關(guān)類(lèi)型的對(duì)象(繼承同一父類(lèi)或者接口的一組子類(lèi)),由給定的參數(shù)來(lái)決定創(chuàng)建哪種類(lèi)型的對(duì)象。建造者模式是用來(lái)創(chuàng)建一種類(lèi)型的復(fù)雜對(duì)象,可以通過(guò)設(shè)置不同的可選參數(shù),“定制化”地創(chuàng)建不同的對(duì)象。

實(shí)戰(zhàn)題目

在下面的 ConstructorArg 類(lèi)中,當(dāng) isRef 為 true 的時(shí)候,arg 表示 String 類(lèi)型的refBeanId,type 不需要設(shè)置;當(dāng) isRef 為 false 的時(shí)候,arg、type 都需要設(shè)置。請(qǐng)根據(jù)這個(gè)需求,完善 ConstructorArg 類(lèi)。

public class ConstructorArg {
    private boolean isRef;
    private Class type;
    private Object arg;
    // TODO: 待完善...
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

友情鏈接更多精彩內(nèi)容