Joshua Bloch錯了? ——適當改變你的Builder模式實現

注:這一系列都是小品文。它們偏重的並非如何實現模式,而是一系列在模式實現,使用等衆多方面絕對值得思考的問題。若是您僅僅但願知道一個模式該如何實現,那麼整個系列都會讓您失望。若是您但願更深刻地瞭解各個模式的經常使用法,並對各個模式進行深刻地思考,那麼但願您能喜歡這一系列文章。html

 

  在昏黃的燈光下,我開始了晚間閱讀。之因此有這個習慣的主要緣由仍是由於個人睡眠一直不是很好。因此我逐漸養成了在晚九點之後看一下子技術書籍以輔助睡眠的習慣。ios

  今天隨手拿起的是Effective Java的英文第二版。說實話,因爲已經看過了Effective Java的初版,所以我一直沒有將它的第二版放在心上。編程

 

這是Builder麼?設計模式

  在看到第二個條目的時候,我就產生了一個大大的疑惑。該條目說若是一個構造函數或工廠模式擁有太多的可選參數,那麼Builder模式是一個很好的選擇。可是該條目所給出的Builder模式實現卻很是奇怪(Java代碼):編程語言

// JAVA代碼
// Builder Pattern
public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder {
        // Required parameters
        private final int servingSize;
        private final int servings;

        // Optional parameters - initialized to default values
        private int calories = 0;
        private int fat = 0;
        private int carbohydrate = 0;
        private int sodium = 0;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

        public Builder calories(int val)
        { calories = val; return this; }

        public Builder fat(int val)
        { fat = val; return this; }

        public Builder carbohydrate(int val)
        { carbohydrate = val; return this; }

        public Builder sodium(int val)
        { sodium = val; return this; }

        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

  或許您腦中的疑問和我同樣,這是Builder麼?函數

 

標準的Builder實現ui

  既然有了這個疑問,我就開始在腦中回憶起Builder模式標準實現的類圖:this

  在該類圖中主要有兩部分組成:Director以及Builder。Director用來制定產品的建立步驟,而Builder則用來爲Director提供產品的各個組件。而在這兩部分組成中,Director表示的是產品組裝步驟,是Builder模式中的不變。而Builder類則是一個基類。各個ConcreteBuilder從它派生並定義組成產品的各個組成,是Builder模式中變化的部分,也是Builder模式中能夠擴展的部分。spa

  所以,其標準實現應以下所示:設計

// C++代碼
#include <iostream>

using namespace std;

class Builder;
class Director;
class Product;
class ConcreteBuilder;

// Builder的公共接口,提供接口給Director以容許調用Builder類的各成員以控制流程
class Builder
{
    // 因爲各個build*函數須要按照必定次序調用才能成功地建立產品,所以爲了不
    // 因爲外部誤調用而影響狀態機,所以將Builder的各個build*函數設置爲私有
    // 並聲明Director爲其友元類
    friend class Director;
private:
    // Builder中的各個build*函數通常無返回值。這是由於每次build*的結果實際上
    // 與所建立的產品相關。若是將其做爲返回值返回,那麼就會強制要求全部的
    // ConcreteBuilder返回同一類型數據,並且Director也須要知道並使用這些數據,
    // 進而形成了Director,Builder以及產品之間的耦合
    virtual void buildPartA() = 0;
    virtual void buildPartB() = 0;

public:
    virtual Product* GetResult() = 0;
};

// 控制產品的建立流程,是Builder模式中的不變
class Director
{
    Builder* m_pBuilder;
public:
    Director(Builder* pBuilder) {
        m_pBuilder = pBuilder;
    }

    // 啓動Builder模式的產品建立流程,而具體建立方式則由Builder類自行決定
    void Construct() {
        m_pBuilder->buildPartA();
        m_pBuilder->buildPartB();
    }
};

class Product
{
    // 因爲產品的建立都是經過ConcreteBuilder來完成的,所以聲明產品類的各個
    // 成員爲私有,並聲明ConcreteBuilder爲其友元,從而達到只容許經過
    // ConcreteBuilder建立產品實例的目的
    friend class ConcreteBuilder;
private:
    struct PartA {};
    struct PartB {};

    // 傳入指針,而不是引用,以容許某些part爲空的狀況
    Product(PartA* pPartA, PartB* pPartB)
    {
        ……
    }

public:
    void printInfo();
};

// Builder的實際實現
class ConcreteBuilder : public Builder
{
    Product::PartA* m_pPartA;
    Product::PartB* m_pPartB;
private:
    // 重寫私有虛函數以提供實際的組成的實際建立邏輯。私有並不會阻止虛函數的
    // 調用及重寫。這是兩個徹底不相干的特性,彼此不會相互影響,也不會因爲私有
    // 函數沒法被派生類訪問而沒法被重寫
    virtual void buildPartA();
    virtual void buildPartB();

public:
    virtual Product* GetResult();
};

void ConcreteBuilder::buildPartA()
{
    m_pPartA = new Product::PartA();
};

void ConcreteBuilder::buildPartB()
{
    m_pPartB = new Product::PartB();
};

Product* ConcreteBuilder::GetResult()
{
    return new Product(m_pPartA, m_pPartB);
};

void Product::printInfo()
{
    cout << "Product constructed by builder pattern." << endl;
};

int _tmain(int argc, _TCHAR* argv[])
{
    // 建立Builder及Director,並經過調用Director的Construct()函數來建立實例
    Builder* pBuilder = new ConcreteBuilder();
    Director* pDirector = new Director(pBuilder);
    pDirector->Construct();

    // 經過調用Builder的GetResult()函數獲得產品實例
    Product* pProduct = pBuilder->GetResult();
    pProduct->printInfo();
    return 0;
}

 

Joshua沒有錯

  「標準實現和Joshua所提供的Builder模式實現居然有如此大的差異,難道是Joshua錯了嗎?」我躺在牀上想到。仔細地查看了Joshua所提供的Builder模式實現,發現其和標準的Builder模式有如下一系列不一樣:

  • 沒有Director類,對產品的建立是經過Builder的build()函數來完成的。
  • 沒有基類Builder,而每一個ConcreteBuilder都被實現爲產品的嵌套類。

  那省略掉的這兩個組成在Builder模式中都是用來作什麼的呢?在Builder模式中,Director用來表示一個產品的固定的建立步驟,它操做的是基類Builder所定義的接口。該接口定義了Director和各個ConcreteBuilder進行溝通的契約,而各個ConcreteBuilder都須要按照這些接口來組織本身的產品建立邏輯。

  也就是說,Director和各個Builder之間的關係實際上就是對產品建立這一個任務執行開閉原則(Open-Close Principle)所產生的結果:Director和基類Builder定義了產品建立的「閉」,即固定的不該被修改的邏輯。而各個ConcreteBuilder則經過從基類Builder派生來自行定義產品中的各個組成的建立邏輯,也便是Builder模式中的「開」。這樣Director中所定義的產品建立步驟能夠被各個產品的建立過程重用了。

  而對Director和基類Builder的省略實際上就是將Builder中固定的產品建立步驟省略了,剩下的僅僅是開放的用來建立產品的實際邏輯。這實際上就是Builder模式中產品建立步驟退化所產生的效果。

  「既然Builder模式已經退化成了單個的彼此再也不相關的類,那它還叫Builder模式麼?」我問本身。顯然,從開閉原則的角度來解釋僅僅能說明這種使用方法能夠被認爲是從Builder模式演化過來的,卻不能說服我這是一個Builder模式。

  我再次拿起了書,想從書中尋找一些線索。在讀到這節中間的時候,我便有了答案。該條目所說的其實是在利用Builder模式中各個ConcreteBuilder的一個特性:若是將Builder中的各個ConcreteBuilder看成是一個Context,那麼其將在可選值方面提供較大的靈活度。

  全部的一切都是從一個很是複雜的構造函數開始提及的。若是建立一個對象須要向構造函數中傳入很是多的參數,並且有些參數是可選的,那麼爲了使用方便,咱們須要提供一個包含了全部參數的構造函數:

// Java代碼
public class NutritionFacts {
    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
            ……
    }
}

  在這種狀況下,咱們就須要按照以下的方法對該構造函數進行調用:

NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);

  但這種方法對可選的養分成分而言並不友好。所以另外一種選擇被提出了,那就是JavaBean模式:

// Java代碼
public class NutritionFacts {
    public void setServingSize(int servingSize) …...
    public void setServings(int servings) ……
    public void setCalories(int calories) ……
    ……
}

  但這種解決方案仍是有問題,那就是各個參數之間的關聯關係。例如食物中全部的卡路里其實是與該食物的重量以及單位重量中所包含的卡路里相關的。所以咱們還須要在setCalories(),setServings()以及setServingSize()中執行輸入數據是否正確的檢查。而這些檢查須要放在哪裏呢?setCalories()等函數中?那麼這些檢查邏輯須要考慮到calories,servings以及servingSize等參數尚未被設置的狀況,並且每次對這些數據的更改都會致使該檢查的執行。

  Joshua提出的解決方案則是Builder模式。該方案所利用的就是Builder模式中的ConcreteBuilder能夠很好地處理可選組成並支持數據檢查的特性:

// Java代碼
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                                 .calories(100)
                                 .sodium(35)
                                 .carbohydrate(27)
                                 .build();

  上面的代碼主要分爲三個部分:對NutritionFacts.Builder的建立,經過.calories()等函數對可選組成的設置,以及經過.build()函數建立NutritionFacts實例。其中在建立NutritionFacts.Builder時咱們須要爲該類型的構造函數指定構造函數所須要的參數,實際上也就是在指定各個必選組成。接下來,咱們就能夠根據須要調用.calories()等函數完成對可選參數的設置。這兩部分代碼實際上就是在對各個必選組成和可選組成進行處理。而最後對.build()函數的調用則用來建立NutritionFacts實例,也是在該解決方案中執行各設置檢查的地方。

  簡單地說,在Builder模式中,ConcreteBuilder具備以下兩個特色:

  • 很是適合處理一個實例具備一系列可選組成的狀況
  • 能夠在建立產品實例前執行額外的自定義邏輯

  這些特色實際上在Gang of Four的設計模式一書中並無被顯式說起,而Joshua卻對這些特徵好好地加以了利用。

  「啊」,我恍然大悟。實際上並非Joshua不知道一個標準的Builder模式是如何實現的。只是由於這個條目中所須要處理的狀況實際上能夠經過Builder模式中的ConcreteBuilder一個組成就可以解決這種問題,所以他提供了一個簡化的,或者說是退化的Builder模式實現,從而更清楚地代表本身的想法。反過來,若是各個產品的建立步驟相同,咱們仍然能夠很容易地抽象出一個基類Builder,併爲公有的建立步驟添加相應的Director。

 

Fluent Interface

  可是Joshua給出的Builder模式中,另外一處實現引發了個人注意。在Builder類中,他使用了Fluent Interface模式:

// Java代碼
public Builder sodium(int val)
{ sodium = val; return this; }

  這是在Martin Fowler的一篇文章中所列出的一種模式。該模式的最大優勢就是大大地提升了代碼的可讀性。在一個標準的Fluent Interface模式實現的幫助下,軟件開發人員能夠編寫出很是易懂的代碼。可是從Joshua給出的示例來看,彷佛這種可讀性的提升並不明顯:

// Java代碼
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
    .calories(100).sodium(35).carbohydrate(27).build();

  當其它軟件開發人員遇到該段代碼的時候,他馬上理解函數調用calories(),sodium(),carbohydrate()等函數的意義麼?

  「若是是我,我會使用一個’with-’前綴吧」,我想到:

// Java代碼
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
    .withCalories(100).withSodium(35).withCarbohydrate(27).build();

  這樣這些函數中所使用的小小的前綴「with-」就能讓其餘軟件開發人員在閱讀食品的養分成分時在腦中所造成相應的語義:這份養分成分表中有100卡路里,35毫克鈉,以及27克碳水化合物。

  固然,這只是一部分人在使用Fluent Interface模式時一種經常使用的命名規範。因爲咱們在平常生活中所使用的語言則不只僅有「XX包含什麼」這種表述,更須要表達「在什麼狀況下」,「何時」等一系列條件。所以像「where-」,「when-」等前綴也是經常用到的。

  固然,計算機語言和天然語言之間仍是有必定的差距的。確切來講,是很大的差距。這種差距的根源主要是因爲咱們天天所說的語言不少時候都沒有編程語言那麼嚴謹。所以在實現Fluent Interface模式的時候,要儘可能平衡使用Fluent Interface模式組織代碼所帶來的額外負擔以及從Fluent Interface模式所帶來的可讀性以及可維護性的提升。

  「拿使用Fluent Interface模式後有沒有什麼損失呢?」我躺在牀上本身問本身。因爲Fluent Interface模式是使用在各個Builder之上的,所以首先我就開始思考它的擴展性是否會受到影響。

  雖說Fluent Interface模式並不要求返回的都是當前實例,可是在Builder模式中,Fluent Interface中的各個接口所返回的經常是Builder類實例自身:

// Java代碼
public Builder withSodium(int val)
{ sodium = val; return this; }

  這顯示了Fluent Interface模式的另外一個問題,那就是對派生並不友好。從上面的代碼能夠看到,該函數所返回的是一個Builder類實例。若是咱們但願從Builder類派生,那麼對Builder類實例所提供的函數的調用就須要放到最後:

// Java代碼
AllNutritionFacts.Builder(240, 8).withTotalEnergy(1400)
        .withNote(「飲料內如有部分沉澱爲果肉,並不影響飲用」)
        .withCalories(100).withSodium(35).withCarbohydrate(27).build();

  這彷佛就不太合常理了:由NutritionFacts.Builder類所提供的最主要養分成分居然被放到了最後。這實際上並無提升什麼可讀性,反而會使得其它軟件開發人員看到這段代碼時感到困惑。

  固然,咱們還能夠嘗試利用C++中有關虛函數的一個特殊性質:若是一個派生類中重寫了基類中的虛函數,那麼該虛函數的返回值能夠適當發生改變。例如在基類中的虛函數返回基類指針或引用的時候,派生類中的相應的虛函數能夠返回派生類的指針或引用。

class Base
{
public:
    // 基類中定義一個虛函數,返回類型是Base類的引用
    virtual Base& self() { return *this; }
};

class Derive : public Base
{
public:
    // 派生類中重寫虛函數,返回類型是Derive類的引用
    virtual Derive& self() { return *this; }
};

  這樣,咱們能夠經過重寫基類中的虛函數,使其返回派生類實例來部分解決Fluent Interface模式對派生不友好的狀況。這種技術也被稱爲Covariant Return Type

  另外一種解決方案就是儘可能使用組合,而不是派生。也就是說,若是Builder模式中的產品類能夠由組合來完成,而不是派生,那麼它就能夠經過各個組成的Builder 來完成對各個組成的生產,再經過自身的Builder來產生最後的產品:

// Java代碼
Benz.Builder()
    .withBody(BenzBody.Builder()
        .withColor()
        .withDoorCount()
        .build())
    .withEngine(Engine.Builder()
        .withPower()
        .build())
    .withWheel(Wheel.Builder()
        .withSize()
        .build())
    .build();

  這樣,各個子組成經過定義本身的Builder一方面能夠提升重用性,另外一方面也能夠經過組合的方式避免使用繼承,進而在按照Fluent Interface組織接口時遇到麻煩。

 

同系列其它文章:http://www.cnblogs.com/loveis715/category/672735.html

轉載請註明原文地址並標明轉載:http://www.cnblogs.com/loveis715/p/4539505.html

商業轉載請事先與我聯繫:silverfox715@sina.com

相關文章
相關標籤/搜索