设计模式--Builder模式的思考


在日常开发中总是会遇到多参数的情况,那么对于多参数,尤其是可选参数众多的情况,可能有如下的一些解决方案.

重叠构造器模式

重叠构造器模式在Java代码中很常见,其解决的问题是参数过多情况下又不想给调用方带来过多的实例化对象负担.在这种情况下调用方只需要选择一个适合自己的构造函数调用就好.

public Configuration(Integer maxConnect) {
  this(maxConnect, 0);
}

public Configuration(Integer maxConnect, Integer minConnect) {
  this("default", maxConnect, minConnect);
}

public Configuration(String password, Integer maxConnect, Integer minConnect) {
  this(...)
}

然而事实总不是那么如人所愿,这个模式有很多缺点.

  1. 层层嵌套,导致整个实例化过程其实是一条直线,一通到底,也就注定了其过程不够灵活.
  2. 对于参数较少的构造函数不得不弄一堆的默认值填充,导致其看起来不是很优雅.
  3. 增加参数对于这种模式无疑是困难重重,需要从底到上一层一层修改.

工厂模式

工厂模式本意在于封装具体的创建流程,提供出简单便捷的入口,但是在多参数情况下其能改进的只是让实例化过程不再是一条直线,工厂中可以根据具体参数制造出Configuration及其子类.其本质与重叠构造器模式并没有太大的区别,只是把构造器逻辑提取到相应工厂,所以工厂模式并不能解决上述问题.

public static Configuration newInstance(String password) {
   return new Configuration("default", password, "default");
 }

 public static Configuration newInstance(String password, String username) {
   return new Configuration(username, password, "default");
 }

 public static Configuration newInstance(String password, String username, String url,) {
   return new Configuration(username, password, url);
 }

JavaBean模式

严格的JavaBean是只有空构造函数,其他属性一律使用set方法,当然必要参数可以放在构造函数中,那么就变成下面的这种形式.

Configuration configuration = new Configuration();
configuration.setPassword("default");
configuration.setUrl("http://mrdear.cn");
configuration.setUsername("default");

虽然少了冗长的参数列表,但是缺点也是很明显:

  1. 对象的创建过程被分解,按照意图,new的过程就是创建,剩下的一律不算创建,但这种模式下的创建实际上是两步,创建与填值.
  2. 对修改开放,该模式暴露了过多set方法,使得任意能获取到该实例的地方都可以随意修改器内容,对于全局性的config实例或者其他单例实例这是致命的缺点.

Builder模式

有句话说得好,遇到难以解决的问题就加一层中间层来代理抽象.Builder模式正式如此,对象本身创建麻烦,那么就使用一个代理对象来主导创建与检验,兼顾了重叠器模式的安全性以及JavaBean模式的灵活性.

public class Configuration {
  private String username;
  private String password;
  private String url;

  public Configuration(String username, String password, String url) {
    this.username = username;
    this.password = password;
    this.url = url;
  }

  public static ConfigurationBuilder builder() {
    return new ConfigurationBuilder();
  }

  public static class ConfigurationBuilder {
    private String username;
    private String password;
    private String url;

    ConfigurationBuilder() {
    }

    public ConfigurationBuilder username(String username) {
      this.username = username;
      return this;
    }

    public ConfigurationBuilder password(String password) {
      this.password = password;
      return this;
    }

    public ConfigurationBuilder url(String url) {
      this.url = url;
      return this;
    }

    public Configuration build() {
      // can some check
      return new Configuration(this.username, this.password, this.url);
    }

    public String toString() {
      return new StringBuilder().append((String)"Configuration.ConfigurationBuilder(username=").append((String)this.username).append((String)", password=").append((String)this.password).append((String)", url=").append((String)this.url).append((String)")").toString();
    }
  }
}

如上面代码,客户端使用Builder对象选择必要的参数,然后最后build()构建出自己想要的参数.Builder有很多优势,也很灵活:

  1. 把线性的构造结构用build方法变成了分支结构,你可以使用build构造该类的子类以及其他相关类.
  2. 很灵活,组合的形式可以在各自builder加强约束校验,并且这些业务逻辑不会在污染你的原类.当不符合的参数应及时抛出IllegalArgumentException
  3. 可作为参数传递,比如Mybatis中就大量使用了这种传递方式让客户端更加方便的构造配置类.
  4. 使用.filed()形式构建参数,只要命名有一定规范,就很清楚参数的作用,编写出来的代码也更加容易阅读,不用点进去看具体参数来选择适合自己的方法了.

当然缺点也有:

  1. 构造想要的类之前必须构造一个builder中间类,对于一些经常循环中实例化的类是很不适合的.大量对象被重复创建会带来性能上的影响.因此对于一些复杂的配置类使用builder时最合适不过的了.

Mybatis中Builder模式应用

Mybatis拥有种类繁多的配置,那么builder就很适合其配置类对象,以MappedStatement类为例子. MappedStatement拥有数十项配置,如果使用构造函数或者静态工厂那么对于开发人员可能是难以接受的体验.一大堆参数,还需要点进去才能知道每一个参数的意义,在这样的情况下Builder模式就是一个很好的解决方式.

public final class MappedStatement {

  private String resource;
  private Configuration configuration;
  private String id;
  private Integer fetchSize;
  private Integer timeout;
  private StatementType statementType;
  private ResultSetType resultSetType;
  private SqlSource sqlSource;
    ......
}

org.apache.ibatis.mapping.MappedStatement.Builder作为MappedStatement的静态内部类,拥有可以访问MappedStatement任意属性的权利.那么其就可以直接实例化mappedStatement对象,然后使用该对象直接访问属性,从而简化Builder模式,也很好的创建出MappedStatement的实例.

  public static class Builder {
    private MappedStatement mappedStatement = new MappedStatement();

    public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
      mappedStatement.configuration = configuration;
      mappedStatement.id = id;
      mappedStatement.sqlSource = sqlSource;
      mappedStatement.statementType = StatementType.PREPARED;
      mappedStatement.parameterMap = new ParameterMap.Builder(configuration, "defaultParameterMap", null, new ArrayList<ParameterMapping>()).build();
      mappedStatement.resultMaps = new ArrayList<ResultMap>();
      mappedStatement.sqlCommandType = sqlCommandType;
      mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
      String logId = id;
      if (configuration.getLogPrefix() != null) {
        logId = configuration.getLogPrefix() + id;
      }
      mappedStatement.statementLog = LogFactory.getLog(logId);
      mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();
    }

    public Builder resource(String resource) {
      mappedStatement.resource = resource;
      return this;
    }
    ......
}

另外为了保证MappedStatement对象必须使用Builder来控制,代码中把其构造函数声明为包级别权限

MappedStatement() {
  // constructor disabled
}

总结

Builder模式本质上是一种特殊的工厂模式,按照流水线方式调用,然后最后检查产品是否合格,流水线之间可以任意组合,达到了高度的灵活性.

参考

Effective Java : 遇到多个构造器参数时考虑构建器(Builder模式)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏精讲JAVA

JDK 10 的 109 项新特性

虽然感觉 JDK9 发布才仅仅几周的时间,然而,随着新的 OpenJDK 的发布节奏,JDK10 已经到达发布候选里程碑阶段。

1482
来自专栏滕先生的博客

runtime官方文档翻译版本通过OC源代码通过NSObject中定义的方法直接调用运行时的函数消息传递机制使用隐藏参数获取方法地址动态方法解析动态加载消息转发转发和多继承代理对象转发和继承类型编码声

2857
来自专栏Java Web

Java 7的新特性

前言 看大佬推荐的书单买了一本《Java 8实战》,总觉得在了解Java 8之前,是不是也应该去了解了解一下Java 7的一些特性?所以就自己百度了一些资料来...

3345
来自专栏不会写文章的程序员不是好厨师

日志那些事儿——Logback源码解析

在上篇文章日志漫谈中谈到,日志在监控报警、查错分析等方面有着非常重要的应用。Logback作为目前最火的日志系统,本文就简单分析一下logback日志打印的过程...

3922
来自专栏java系列博客

eclipse的一些小问题解决方案

2369
来自专栏PHP在线

PHP 底层的运行机制与原理

原文出处: nowamagic 欢迎分享原创到伯乐头条 PHP说简单,但是要精通也不是一件简单的事。我们除了会使用之外,还得知道它底层的工作原理。 PHP是...

3967
来自专栏Java学习网

高性能Java解析器实现过程详解

高性能Java解析器实现过程详解 如果你没有指定数据或语言标准的或开源的Java解析器, 可能经常要用Java实现你自己的数据或语言解析器。或者,可能有很多解析...

3866
来自专栏冰霜之地

深入浅出 FlatBuffers 之 Schema

FlatBuffers 是一个序列化开源库,实现了与 Protocol Buffers,Thrift,Apache Avro,SBE 和 Cap'n Proto...

2912
来自专栏java学习

java基础笔记1

Java基础 | 数据库 | Android | 学习视频 | 学习资料下载 最新通知 按照我去培训机构的学习经历,给初学还有自学Java 的同学一个基本的学习...

3797
来自专栏不二小段

Python为什么文件运行和在命令行运行同样语句但结果却不同?

这篇是之前知乎上的一个提问,感觉非常有趣而且内容丰富,所以把我自己的回答搬运到公众号来。 另外关于昨天的推送,是因为我之前把文章投到了Python中文社区的公众...

39113

扫码关注云+社区

领取腾讯云代金券