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

相关文章
相关标签/搜索