首页 > 其他分享 >设计模式系列 | 建造者模式

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

时间:2023-06-08 22:31:42浏览次数:30  
标签:系列 int 建造 private final servings 设计模式 servingSize public

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

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

定义

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

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 类中:

设计模式系列 | 建造者模式_敏捷开发_02

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

总结

建造者模式的类图

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

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

建造者模式优缺点

建造者模式的优点有:

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

建造者模式的缺点有:

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

角色及其职责

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

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

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

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

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


标签:系列,int,建造,private,final,servings,设计模式,servingSize,public
From: https://blog.51cto.com/u_11702014/6443961

相关文章

  • Dubbo系列<3>-服务提供端与消费端应用的搭建
    创建一个工程dubbo,其中一共分三个module:provider:服务提供者consumer:服务消费者api:是针对服务的接口和实体install成jar给provider和consumer使用1:基于配置方式调用api结构和代码如下:importjava.io.Serializable;/***用户信息**@Authortianweichang*@Date2018-08-1......
  • 转载:Spring 框架的设计理念与设计模式分析
    Spring框架的设计理念与设计模式分析<!--LEADSPACE_BODY_END--><!--SUMMARY_BEGIN-->Spring作为现在最优秀的框架之一,已被广泛的使用,并且有很多对其分析的文章。本文将从另外一个视角试图剖析出Spring框架的作者设计Spring框架的骨骼架构的设计理念,有那几个核心组件?为......
  • 单例这种设计模式
    随着我们编写代码的深入,我们或多或少都会接触到设计模式,其中单例(Singleton)模式应该是我们耳熟能详的一种模式。本文将比较特别的介绍一下Java设计模式中的单例模式。概念单例模式,又称单件模式或者单子模式,指的是一个类只有一个实例,并且提供一个全局访问点。实现思路在单例的类中......
  • html5游戏制作入门系列教程(五)
    我们继续这一系列文章,使用HTML5的canvas组件进行游戏开发。今天,这是相当完整的游戏例子–它会回顾经典的旧电脑游戏–坦克大战。我会教你使用阵列地图并教你如何检测活动对象(坦克)与环境(基于阵列的地图)的碰撞。你可以点击这里阅读这一系列教程的前一篇文章:html5游戏制作入门系列......
  • html5游戏制作入门系列教程(二)
    今天,我们继续html5游戏制作入门系列的系列文章。今天,我们将继续基础知识(也许甚至是高级技巧的基础)。我要告诉你如何具有渐变颜色填充对象,绘制文本,使用自定义的字体绘制文本,基本的动画,以及最重要的UI元素–按钮。 我们以前的文章中,你可以在这里阅读:html5游戏制作入门系列教程(一)。......
  • html5游戏制作入门系列教程(一)
    从今天开始,我们将开始HTML5游戏开发一系列的文章。在我们的第一篇文章中,我们将讲解在画布canvas上的基础工作,创建简单的对象,填充和事件处理程序。另外,要注意在这个阶段中,我们不会立即学习WebGL相关的3D部分。但我们会尽快在未来的WebGL。 在每篇文章中,我们都将学习到一些新的东西......
  • 使用c#实现23种设计模式
    使用c#实现23种常见的设计模式设计模式通常分为三个主要类别:创建型模式结构型模式行为型模式。这些模式是用于解决常见的对象导向设计问题的最佳实践。以下是23种常见的设计模式并且提供c#代码案例:创建型模式:1.单例模式(Singleton)publicsealedclassSingleton......
  • Redis系列15:使用Stream实现消息队列(精讲)
    Redis系列1:深刻理解高性能Redis的本质Redis系列2:数据持久化提高可用性Redis系列3:高可用之主从架构Redis系列4:高可用之Sentinel(哨兵模式)Redis系列5:深入分析Cluster集群模式追求性能极致:Redis6.0的多线程模型追求性能极致:客户端缓存带来的革命Redis系列8:Bitmap实现亿万级......
  • VSCode 插件开发系列教程
    VSCode插件架构,VSCode是通过Electron实现跨平台的,而Electron则是基于Chromium和Node.js,比如VSCode的界面,就是通过Chromium进行渲染的。同时,VSCode是多进程架构,当VSCode第一次被启动时会创建一个主进程(mainprocess),然后每个窗口,都会创建一个渲染进程(Renderer......
  • 浅谈物联网平台在智慧建造行业的应用前景
    物联网,作为一个跨学科、跨行业的技术领域,无疑对于现代社会的各个领域都产生了深远的影响,智慧建造行业也不例外。随着物联网技术的发展,物联网平台在智慧建造行业的应用也越来越广泛,为行业发展带来了更多可能性。本文将浅谈物联网平台在智慧建造行业的应用前景。智慧建造行业,简单说......