在平时的开发中,创建一个对象最常用的方式是,使用 new 关键字调用类的构造函数来完成。我的问题是,什么情况下这种方式就不适用了,就需要采用建造者模式来创建对象呢?你可以先思考一下,下面我通过一个例子来带你看一下。

假设有这样一道设计面试题:我们需要定义一个资源池配置类 ResourcePoolConfig。这里的资源池,你可以简单理解为线程池、连接池、对象池等。在这个资源池配置类中,有以下几个成员变量,也就是可配置项。现在,请你编写代码实现这个 ResourcePoolConfig 类


只要你稍微有点开发经验,那实现这样一个类对你来说并不是件难事。最常见、最容易想到的实现思路如下代码所示。因为 maxTotal、maxIdle、minIdle 不是必填变量,所以在创建 ResourcePoolConfig 对象的时候,我们通过往构造函数中,给这几个参数传递 null 值,来表示使用默认值。


public class ResourcePoolConfig {private static final int DEFAULT_MAX_TOTAL = 8;private static final int DEFAULT_MAX_IDLE = 8;private static final int DEFAULT_MIN_IDLE = 0;private String name;private int maxTotal = DEFAULT_MAX_TOTAL;private int maxIdle = DEFAULT_MAX_IDLE;private int minIdle = DEFAULT_MIN_IDLE;public ResourcePoolConfig(String name, Integer maxTotal, Integer maxIdle, Integer minIdle) {if (StringUtils.isBlank(name)) {throw new IllegalArgumentException("name should not be empty.");}this.name = name;if (maxTotal != null) {if (maxTotal <= 0) {throw new IllegalArgumentException("maxTotal should be positive.");}this.maxTotal = maxTotal;}if (maxIdle != null) {if (maxIdle < 0) {throw new IllegalArgumentException("maxIdle should not be negative.");}this.maxIdle = maxIdle;}if (minIdle != null) {if (minIdle < 0) {throw new IllegalArgumentException("minIdle should not be negative.");}this.minIdle = minIdle;}}//...省略getter方法...
}

现在,ResourcePoolConfig 只有 4 个可配置项,对应到构造函数中,也只有 4 个参数,参数的个数不多。但是,如果可配置项逐渐增多,变成了 8 个、10 个,甚至更多,那继续沿用现在的设计思路,构造函数的参数列表会变得很长,代码在可读性和易用性上都会变差。在使用构造函数的时候,我们就容易搞错各参数的顺序,传递进错误的参数值,导致非常隐蔽的 bug。


// 参数太多,导致可读性差、参数可能传递错误
ResourcePoolConfig config = new ResourcePoolConfig("dbconnectionpool", 16, null, 8, null, false , true, 10, 20,false, true);

解决这个问题的办法你应该也已经想到了,那就是用 set() 函数来给成员变量赋值,以替代冗长的构造函数。我们直接看代码,具体如下所示。其中,配置项 name 是必填的,所以我们把它放到构造函数中设置,强制创建类对象的时候就要填写。其他配置项 maxTotal、maxIdle、minIdle 都不是必填的,所以我们通过 set() 函数来设置,让使用者自主选择填写或者不填写。


public class ResourcePoolConfig {private static final int DEFAULT_MAX_TOTAL = 8;private static final int DEFAULT_MAX_IDLE = 8;private static final int DEFAULT_MIN_IDLE = 0;private String name;private int maxTotal = DEFAULT_MAX_TOTAL;private int maxIdle = DEFAULT_MAX_IDLE;private int minIdle = DEFAULT_MIN_IDLE;public ResourcePoolConfig(String name) {if (StringUtils.isBlank(name)) {throw new IllegalArgumentException("name should not be empty.");}this.name = name;}public void setMaxTotal(int maxTotal) {if (maxTotal <= 0) {throw new IllegalArgumentException("maxTotal should be positive.");}this.maxTotal = maxTotal;}public void setMaxIdle(int maxIdle) {if (maxIdle < 0) {throw new IllegalArgumentException("maxIdle should not be negative.");}this.maxIdle = maxIdle;}public void setMinIdle(int minIdle) {if (minIdle < 0) {throw new IllegalArgumentException("minIdle should not be negative.");}this.minIdle = minIdle;}//...省略getter方法...
}

接下来,我们来看新的 ResourcePoolConfig 类该如何使用。我写了一个示例代码,如下所示。没有了冗长的函数调用和参数列表,代码在可读性和易用性上提高了很多。


// ResourcePoolConfig使用举例
ResourcePoolConfig config = new ResourcePoolConfig("dbconnectionpool");
config.setMaxTotal(16);
config.setMaxIdle(8);

至此,我们仍然没有用到建造者模式,通过构造函数设置必填项,通过 set() 方法设置可选配置项,就能实现我们的设计需求。如果我们把问题的难度再加大点,比如,还需要解决下面这三个问题,那现在的设计思路就不能满足了。

  • 我们刚刚讲到,name 是必填的,所以,我们把它放到构造函数中,强制创建对象的时候就设置。如果必填的配置项有很多,把这些必填配置项都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果我们把必填项也通过 set() 方法设置,那校验这些必填项是否已经填写的逻辑就无处安放了.

  • 除此之外,假设配置项之间有一定的依赖关系,比如,如果用户设置了 maxTotal、maxIdle、minIdle 其中一个,就必须显式地设置另外两个;或者配置项之间有一定的约束条件,比如,maxIdle 和 minIdle 要小于等于 maxTotal。如果我们继续使用现在的设计思路,那这些配置项之间的依赖关系或者约束条件的校验逻辑就无处安放了。

  • 如果我们希望 ResourcePoolConfig 类对象是不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值。要实现这个功能,我们就不能在 ResourcePoolConfig 类中暴露 set() 方法。

为了解决这些问题,建造者模式就派上用场了。

我们可以把校验逻辑放置到 Builder 类中,先创建建造者,并且通过 set() 方法设置建造者的变量值,然后在使用 build() 方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象。除此之外,我们把 ResourcePoolConfig 的构造函数改为 private 私有权限。这样我们就只能通过建造者来创建 ResourcePoolConfig 类对象。并且,ResourcePoolConfig 没有提供任何 set() 方法,这样我们创建出来的对象就是不可变对象了。

我们用建造者模式重新实现了上面的需求,具体的代码如下所示:


public class ResourcePoolConfig {private String name;private int maxTotal;private int maxIdle;private int minIdle;private ResourcePoolConfig(Builder builder) {this.name = builder.name;this.maxTotal = builder.maxTotal;this.maxIdle = builder.maxIdle;this.minIdle = builder.minIdle;}//...省略getter方法...//我们将Builder类设计成了ResourcePoolConfig的内部类。//我们也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。public static class Builder {private static final int DEFAULT_MAX_TOTAL = 8;private static final int DEFAULT_MAX_IDLE = 8;private static final int DEFAULT_MIN_IDLE = 0;private String name;private int maxTotal = DEFAULT_MAX_TOTAL;private int maxIdle = DEFAULT_MAX_IDLE;private int minIdle = DEFAULT_MIN_IDLE;public ResourcePoolConfig build() {// 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等if (StringUtils.isBlank(name)) {throw new IllegalArgumentException("...");}if (maxIdle > maxTotal) {throw new IllegalArgumentException("...");}if (minIdle > maxTotal || minIdle > maxIdle) {throw new IllegalArgumentException("...");}return new ResourcePoolConfig(this);}public Builder setName(String name) {if (StringUtils.isBlank(name)) {throw new IllegalArgumentException("...");}this.name = name;return this;}public Builder setMaxTotal(int maxTotal) {if (maxTotal <= 0) {throw new IllegalArgumentException("...");}this.maxTotal = maxTotal;return this;}public Builder setMaxIdle(int maxIdle) {if (maxIdle < 0) {throw new IllegalArgumentException("...");}this.maxIdle = maxIdle;return this;}public Builder setMinIdle(int minIdle) {if (minIdle < 0) {throw new IllegalArgumentException("...");}this.minIdle = minIdle;return this;}}
}// 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
ResourcePoolConfig config = new ResourcePoolConfig.Builder().setName("dbconnectionpool").setMaxTotal(16).setMaxIdle(10).setMinIdle(12).build();

实际上,使用建造者模式创建对象,还能避免对象存在无效状态。我再举个例子解释一下。比如我们定义了一个长方形类,如果不使用建造者模式,采用先创建后 set 的方式,那就会导致在第一个 set 之后,对象处于无效状态。具体代码如下所示:


Rectangle r = new Rectange(); // r is invalid
r.setWidth(2); // r is invalid
r.setHeight(3); // r is valid

为了避免这种无效状态的存在,我们就需要使用构造函数一次性初始化好所有的成员变量。如果构造函数参数过多,我们就需要考虑使用建造者模式,先设置建造者的变量,然后再一次性地创建对象,让对象一直处于有效状态。

实际上,如果我们并不是很关心对象是否有短暂的无效状态,也不是太在意对象是否是可变的。比如,对象只是用来映射数据库读出来的数据,那我们直接暴露 set() 方法来设置类的成员变量值是完全没问题的。而且,使用建造者模式来构建对象,代码实际上是有点重复的,ResourcePoolConfig 类中的成员变量,要在 Builder 类中重新再定义一遍。

总结

  • 类属性过多又要复杂的校验时可以采用建造者模式

参考

46 | 建造者模式:详解构造函数、set方法、建造者模式三种对象创建方式

为什么需要建设者模式相关推荐

  1. swift 听筒模式_Swift的建设者模式

    swift 听筒模式 If you've ever faced an init with too many parameters or objects whose creation is dictat ...

  2. 使用建造者模式创建模拟数据

    前言 在写测试用例时,我们经常需要创建模拟数据,在C#中常用的方式是使用nuget包Bogus. Bogus可以按照一定规则生成随机数据,示例代码如下: public class User {publ ...

  3. swift 听筒模式_Swift中的“复合”模式

    swift 听筒模式 定义 (Definition) 'Composite' pattern is a structural design pattern that is useful for com ...

  4. swift 听筒模式_Swift中的存储库模式

    swift 听筒模式 重点 (Top highlight) 背景 (Background) All apps developed require data of some description. T ...

  5. swift 听筒模式_Swift中的“工厂方法”模式

    swift 听筒模式 定义 (Definition) 'Factory Method' pattern is a creational design pattern that abstracts th ...

  6. 爪哇语言单态创立性模式介绍

    什么是模式 一个围棋下得好的人知道,好的"形"对于围棋非常重要.形是棋子在棋盘上的几何形状的抽象化. 形就是模式(Pattern),也是人脑把握和认识外界的关键.而人脑对处理模式的 ...

  7. java值传递string_关于java:按值传递(StringBuilder与String)

    本问题已经有最佳答案,请猛点这里访问. 我不明白为什么system.out.println(name)在不受方法的concat函数影响的情况下输出sam,而system.out.println(nam ...

  8. 类设计原则及设计模式(一篇就够)

    类设计原则及设计模式 类设计的六大原则 设计模式定义 设计模式的分类 创建型模式 1. 简单工厂和工厂方法模式 定义和分类 2. 抽象工厂模式 3. 单例模式 定义 优缺点 饿汉式单例与懒汉式单例类比 ...

  9. Mybatis 动态切换数据库

    mybatis介绍: 每一个Mybatis的应用程序都以一个SqlSessionFactory对象的实例为核心.SqlSessionFactory对象实例可以通过SqlSessionFactoryBu ...

最新文章

  1. 【NetApp】重删和压缩的关系
  2. shell date 格式化
  3. 工作单元php,php – 无法从工作单元测试用例构建最简单的套件
  4. 解析提高PHP执行效率的50个技巧(转)
  5. python自带的idle输入python_打开python自带IDLE出的问题
  6. leetcode-345-Reverse Vowels of a String
  7. lisp一键室内标注_LISP-标注的自动位置调整
  8. 2015蓝桥杯C++A:饮料换购
  9. Layui table隐藏某一列
  10. 在浏览器中输入url地址 - 显示主页的过程
  11. webpack项目使用eslint建立代码规范
  12. python调用phone库查询手机号码相关信息
  13. 深信服技术认证之Openstack云平台使用入门
  14. 触摸屏设置禁用手指缩放机双击放大功能
  15. c++Windows怎样关机【详解】
  16. SpringCloud的注册中心
  17. class uesrfun.php,帝国cms教程:列表页面批量添加Tags -电脑资料
  18. 计算机硬盘只显示c盘,电脑只显示C盘我们应该怎么办
  19. mpvue开发微信小程序踩坑笔记
  20. iuv_5g组网问题表

热门文章

  1. 有哪些可以远程连接控制云服务器的软件?
  2. linux启动清除指定内存,柴少鹏的官方网站
  3. mysql.net连接器_关于mysql-connector-net在C#中的用法
  4. 微信小程序实现下拉刷新
  5. JavaScript+cesium 添加高德影像图和标注
  6. mysqlplus 批量插入_MySQL批量插入数据
  7. mysql 查询一个数据库里面有多少张数据表
  8. oracle统计每个类别,Oracle统计分析函数集之一
  9. mysql的数据库的索引_MySQL 数据库索引原理与分类
  10. html标签的显示模式(块级标签,行内标签,行内块标签)