设计模式系列 | 建造者模式

想自己的开发路子走得更远更久,想成为更牛的码农,那设计模式的理解和掌握是必须的。

老田,能详细说说你的段位2吗?

很多人也都听说过建造者设计模式,但总是对这个设计模式理解得不够透彻,今天我们就来聊聊建造者设计模式。另外也说说建造者设计模式和工厂模式的区别。

定义

其实建造者设计模式的定义,很多事看不懂的,也是记不住的,但我们还是得先来看看是如何定义的。

The intent of the Builder design pattern is to separate the construction of a complex object from its representation. By doing so the same construction process can create different representations.

将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示

另外在维基百科解释是:

建造者模式 Builder Pattern,又名生成器模式,是一种对象构建模式。它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。

是不是觉得非常的不好理解?

下面我们就用生活中的案例,反过来理解建造者设计模式的定义会更好。

案例1

借用并改造下 Effective Java 中给出的例子:每种食品包装上都会有一个营养成分表,每份的含量、每罐的含量、每份卡路里、脂肪、碳水化合物、钠等,还可能会有其他 N 种可选数据,大多数产品的某几个成分都有值,该如何定义营养成分这个类呢?

重叠构造器

因为有多个参数,有必填、有选填,最先想到的就是定义多个有参构造器:第一个构造器只有必传参数,第二个构造器在第一个基础上加一个可选参数,第三个加两个,以此类推,直到最后一个包含所有参数,这种写法称为重叠构造器,有点像叠罗汉。还有一种常见写法是只写一个构造函数,包含所有参数。

代码如下:

public class Nutrition {
    private int servingSize;// required
    private int servings;// required
    private int calories;// optional
    private int fat;// optional
    private int sodium;// optional
    private int carbohydrate;// optional

public Nutrition(final int servingSize, final int servings) {
        this(servingSize, servings, 0, 0, 0, 0);
    }

public Nutrition(final int servingSize, final int servings, final int calories) {
        this(servingSize, servings, calories, 0, 0, 0);
    }

public Nutrition(final int servingSize, final int servings, final int calories, final int fat) {
        this(servingSize, servings, calories, fat, 0, 0);
    }

public Nutrition(final int servingSize, final int servings, final int calories, final int fat, final int sodium) {
        this(servingSize, servings, calories, fat, sodium, 0);
    }

public Nutrition(final int servingSize, final int servings, final int calories, final int fat, final int sodium, final int carbohydrate) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }

// getter
}

这种写法还可以有效解决参数校验,只要在构造器中加入参数校验就可以了。

如果想要初始化实例,只需要 new 一下就行:

new Nutrition(100, 50, 0, 35, 0, 10)

这种写法,不够优雅的地方是,当 calories 和 sodium 值为 0 的时候,也需要在构造函数中明确定义是 0,示例中才 6 个参数,也能勉强接受。但是如果参数达到 20 个呢?可选参数中只有一个值不是 0 或空,写起来很好玩了,满屏全是 0 和 null 的混合体。

还有一个隐藏缺点,那就是如果同类型参数比较多,比如上面这个例子,都是 int 类型,除非每次创建实例的时候仔细对比方法签名,否则很容易传错参数,而且这种错误编辑器检查不出来,只有在运行时会出现各种诡异错误,排错的时候不知道要薅掉多少根头发了。

想要解决上面两个问题,不难想到,可以通过 set 方法一个个赋值就行了。

set 方式赋值

既然构造函数中放太多参数不够优雅,还有缺点,那就换种写法,构造函数只保留必要字段,其他参数的赋值都用 setter 方法就行了。

代码如下:

public class Nutrition {
    private final int servingSize;// required
    private final int servings;// required
    private int calories;// optional
    private int fat;// optional
    private int sodium;// optional
    private int carbohydrate;// optional

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

// getter and setter
}

这样就可以解决构造函数参数太多、容易传错参数的问题,只在需要的时候 set 指定参数就行了。

如果没有特殊需求,到这里可以解决大部分问题了。

但是需求总是多变的,总会有类似“五彩斑斓的黑”这种奇葩要求:

  1. 如果必填参数比较多,或者大部分参数是必填参数。这个时候这种方式又会出现重叠构造器那些缺点。
  2. 如果把所有参数都用 set 方法赋值,那又没有办法进行必填项的校验。
  3. 如果非必填参数之间有关联关系,比如上面例子中,脂肪 fat 和碳水化合物 carbohydrate 有值的话,卡路里 calories 一定不会为 0。但是使用现在这种设计思路,属性之间的依赖关系或者约束条件的校验逻辑就没有地方定义了。
  4. 如果想要把 Nutrition 定义成不可变对象的话,就不能使用 set 方法修改属性值。

这个时候就该祭出今天的主角了。

建造者模式

先上代码

public class Nutrition {
    private int servingSize;// required
    private int servings;// required
    private int calories;// optional
    private int fat;// optional
    private int sodium;// optional
    private int carbohydrate;// optional

public static class Builder {
        private final int servingSize;// required
        private final int servings;// required
        private int calories;// optional
        private int fat;// optional
        private int sodium;// optional
        private int carbohydrate;// optional

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

public Builder setCalories(final int calories) {
            this.calories = calories;
            return this;
        }

public Builder setFat(final int fat) {
            this.fat = fat;
            return this;
        }

public Builder setSodium(final int sodium) {
            this.sodium = sodium;
            return this;
        }

public Builder setCarbohydrate(final int carbohydrate) {
            this.carbohydrate = carbohydrate;
            return this;
        }

public Nutrition build() {
            // 这里定义依赖关系或者约束条件的校验逻辑
            return new Nutrition(this);
        }
    }

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

// getter
}

想要创建对象,只要调用 new Nutrition.Builder(100, 50).setFat(35).setCarbohydrate(10).build() 就可以了。这种方式兼具前两种方式的优点:

  • 能够毫无歧义且明确 set 指定属性的值;
  • 在 build 方法或 Nutrition 构造函数中定义校验方法,可以在创建对象过程中完成校验。

建造者模式的缺点就是代码变多了(好像所有的设计模式都有这个问题),这个缺点可以借助 Lombok 来解决,通过注解@Builder,可以在编译过程自动生成对象的 Builder 类,相当省事。

案例2

接下来分析下《大话设计模式》中的一个例子,这个例子从代码结构上,和建造者模式有很大的出入,但是作者却把它归为建造者模式。下面我们就来看看究竟:现在需要画个小人,一个小人需要头、身体、左手、右手、左脚、右脚。

代码如下:

public class Person {
    private String head;
    private String body;
    private String leftHand;
    private String rightHand;
    private String leftLeg;
    private String rightLeg;

// getter/setter
}

public class PersonBuilder {
    private Person person = new Person();

public PersonBuilder buildHead() {
        person.setHead("头");
        return this;
    }

public PersonBuilder buildBody() {
        person.setBody("身体");
        return this;
    }

public PersonBuilder buildLeftHand() {
        person.setLeftHand("左手");
        return this;
    }

public PersonBuilder buildRightHand() {
        person.setRightHand("右手");
        return this;
    }

public PersonBuilder buildLeftLeg() {
        person.setLeftLeg("左腿");
        return this;
    }

public PersonBuilder buildRightLeg() {
        person.setRightLeg("右腿");
        return this;
    }

public Person getResult() {
        return this.person;
    }
}

但是,如果有个方法忘记调用了,比如画右手的方法忘记调用了,那就成杨过大侠了。这个时候就需要在 PersonBuilder 之上加一个 Director 类,俗称监工。

public class PersonDirector {
    private final PersonBuilder pb;

public PersonDirector(final PersonBuilder pb) {
        this.pb = pb;
    }

public Person createPerson() {
        this.pb
            .buildHead()
            .buildBody()
            .buildLeftHand()
            .buildRightHand()
            .buildLeftLeg()
            .buildRightLeg();
        return this.pb.getResult();
    }
}

这个时候,对于客户端来说,只需要关注 Director 类就行了,就相当于在客户端调用构造器之间,增加一个监工、一个对接人,保证客户端能够正确使用 Builder 类。

细心的朋友可能会发现,我这里的 Director 类的构造函数增加了一个 Builder 参数,这是为了更好的扩展。

比如,这个时候需要增加一个胖子 Builder 类,那就只需要定义一个 FatPersonBuilder,继承 PersonBuilder,然后只需要将新增加的类传入 Director 的构造函数即可。

这也是建造者模式的另一个优点:可以定义不同的 Builder 类实现不同的构建属性,比如上面的普通人和胖子两个 Builder 类。

框架中的应用

建造者设计模式,在JDK、Mybatis、Spring等框架源码中,得到了大量的应用。

在JDK源码中的应用

JDK 的 StringBuilder 类中提供了 append() 方法,这就是一种链式创建对象的方法,开放构造步骤,最后调用 toString() 方法就可以获得一个完整的对象。StringBuilder 类源码如下:

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence {
    ...
    public StringBuilder append(Object obj) {
        return append(String.valueOf(obj));
    }
    ...
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }
...
}

另外在JDK中还有以下这些也用到了建造者设计模式:

· java.lang.StringBuffer#append()

· java.nio.ByteBuffer#put() (CharBuffer, ShortBuffer, IntBuffer,LongBuffer, FloatBuffer 和DoubleBuffer与之类似)

· javax.swing.GroupLayout.Group#addComponent()

· java.sql.PreparedStatement

· java.lang.Appendable的所有实现类

在Mybatis中的应用

MyBatis 中 SqlSessionFactoryBuiler 类用到了建造者模式。且在 MyBatis 中 SqlSessionFactory是由 SqlSessionFactoryBuilder 产生的,代码如下:

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

DefaultSqlSessionFactory 的构造器需要传入 MyBatis 核心配置类 Configuration 的对象作为参数,而 Configuration 庞大复杂,初始化比较麻烦,因此使用了专门的建造者 XMLConfigBuilder 进行构建。

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
        // 创建建造者XMLConfigBuilder实例
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        // XMLConfigBuilder的parse()构建Configuration实例
        return build(parser.parse());
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
        ErrorContext.instance().reset();
        try {
            inputStream.close();
        } catch (IOException e) {
            // Intentionally ignore. Prefer previous error.
        }
    }
}

XMLConfigBuilder 负责 Configuration 各个组件的创建和装配,整个装配的流程化过程如下:

private void parseConfiguration(XNode root) {
    try {
        //issue #117 read properties first
        // Configuration#
        propertiesElement(root.evalNode("properties"));
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        loadCustomVfs(settings);
        typeAliasesElement(root.evalNode("typeAliases"));
        pluginElement(root.evalNode("plugins"));
        objectFactoryElement(root.evalNode("objectFactory"));
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        reflectorFactoryElement(root.evalNode("reflectorFactory"));
        settingsElement(settings);
        // read it after objectFactory and objectWrapperFactory issue #631
        environmentsElement(root.evalNode("environments"));
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        typeHandlerElement(root.evalNode("typeHandlers"));
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

XMLConfigBuilder 负责创建复杂对象 Configuration,其实就是一个具体建造者角色。SqlSessionFactoryBuilder 只不过是做了一层封装去构建 SqlSessionFactory 实例,这就是建造者模式简化构建的过程。

在Spring中的应用

比如UriComponentsBuilder 类中:

这里就不详细说应用的目的和实现的功能。因为这里还能扯很久,我们只是要知道建造者设计模式的使用也是非常广泛的,由此可知,此设计模式还是相当重要的。

总结

建造者模式的类图

下面是从网上找了一张建造者设计模式的类图:

建造者模式优缺点

建造者模式的优点有:

  • 1、封装性好,创建和使用分离
  • 2、扩展性好,建造类之间独立,一定程度上实现了解耦

建造者模式的缺点有:

  • 1、产生多余的Builder对象
  • 2、产品内部发生变化时,建造者都需要修改,成本较大

角色及其职责

  • Director:指挥者/导演类,负责安排已有模块的顺序,然后告诉Builder开始建造。
  • Builder:抽象建造者,规范产品的组建,一般由子类实现。
  • ConcreteBuilder:具体建造者,实现抽象类定义的所有方法,并且返回一个组建好的对象。
  • Product:产品类,通常实现了模板方法模式。

建造者模式和工厂模式区别

建造者模式优点类似于工厂模式,都是用来创建一个对象,但是他们还是有很大的区别,主要区别如下:

  • 1、建造者模式更加注重方法的调用顺序,工厂模式注重于创建完整对象
  • 2、建造者模式根据不同的产品零件和顺序可以创造出不同的产品,而工厂模式创建出来的产品都是一样的
  • 3、建造者模式使用者需要知道这个产品有哪些零件组成,而工厂模式的使用者不需要知道,直接创建就行

彩蛋

偷偷的告诉你一个小技巧,一旦看到某某类是以Builder结尾的命名,咱们第一印象应该是猜想这里是不是用到了建造者设计模式呢?

推荐阅读

设计模式系列 | 模板方法模式
自学编程的4大误区,你中招了吗?
6000多字 | 秒杀系统设计注意点
给,你们想要的内存溢出MAT排查工具

点赞越多,bug越少

(0)

相关推荐

  • 通俗易懂系列 | 设计模式(八):建造者模式

    介绍# 今天我们将研究java中的Builder模式.Builder 设计模式是一种创造性的设计模式,如工厂模式和抽象工厂模式. 当Object包含许多属性时,引入了Builder模式来解决Facto ...

  • 建造者模式(Bulider模式)详解

    在软件开发过程中有时需要创建一个复杂的对象,这个复杂对象通常由多个子部件按一定的步骤组合而成.例如,计算机是由 CPU.主板.内存.硬盘.显卡.机箱.显示器.键盘.鼠标等部件组装而成的,采购员不可能自 ...

  • ​PHP设计模式之建造者模式

    PHP设计模式之建造者模式 建造者模式,也可以叫做生成器模式,builder这个词的原意就包含了建筑者.开发者.创建者的含义.很明显,这个模式又是一个创建型的模式,用来创建对象.那么它的特点是什么呢? ...

  • 设计模式--Bulider模式

    起因:最近在做统计计算,创建的实体中属性比较多,都是一些数值,一开始是通过get.set方法进行赋值,占用了很多业务代码方法的长度,可读性不太好,后来改用了添加构造器的方式,稍显精简了一点,但是每次赋 ...

  • 建造者模式之项目运用

    建造者模式之项目运用

  • 设计模式模式(四):建造者模式(生成器模式)

    建造者模式主要解决问题: 具备若干成员,当其中一个成员发生变化,其它成员也随着发生变化. 这种复杂对象的生成需要使用建造者模式来生成. 建造者设计模式的结构图: 来源:http://c.bianche ...

  • 设计模式-建造者模式

    建造者模式 也叫生成器模式,他是一个创建型模式 通用类图 Product产品类 ​通常是实现了模板方法模式,也就是有模板方法和基本方法. public class Product { public v ...

  • [PHP小课堂]PHP设计模式之建造者模式

    [PHP小课堂]PHP设计模式之建造者模式 关注公众号:[硬核项目经理]获取最新文章 添加微信/QQ好友:[DarkMatterZyCoder/149844827]免费得PHP.项目管理学习资料

  • 设计模式(4) 建造者模式

    什么是建造者模式 经典建造者模式的优缺点 对建造者模式的扩展 什么是建造者模式 建造者模式将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示.创建者模式隐藏了复杂对象的创建过程 ...

  • 设计模式之建造者模式

    设计模式之建造者模式

  • 设计模式-创建型-建造者模式

    引言: 无论是在现实世界中还是在软件系统中,都存在一些复杂的对象,它们拥有多个组成部分,如汽车,它包括车轮.底盘.发动机.方向盘等各种部件.而对于大部分用户而言,无须知道这些部件的装配细节,也几乎不会 ...

  • 设计模式 | 建造者模式/生成器模式(builder)

    定义: 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示. 结构:(书中图,侵删) 一个产品类 一个指定产品各个部件的抽象创建接口 若干个实现了各个部件的具体实现的创建类 一个 ...