打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
设计模式系列 | 建造者模式

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

老田,能详细说说你的段位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, 0000);
    }

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

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

    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.SerializableCharSequence 
{
    ...
    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越少

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
《Effective Java》笔记
建造者模式之项目运用
《JAVA与模式》之建造模式
建造者模式(Bulider模式)详解
设计模式之创建者模式
78条高质量编码建议《Effective Java》(1~2)阅读笔记
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服