Builder构建者模式,将复杂对象的创建过程与其表示分离,活学活用才是王道

青旅半醒 2023-01-23 11:54 85阅读 0赞

首发CSDN:徐同学呀,原创不易,转载请注明源链接。我是徐同学,用心输出高质量文章,希望对你有所帮助。

文章目录

    • 一、前言
      • 传统创建对象的弊端
    • 二、构建者模式
      • 1、通用写法
      • 2、构建者模式的优缺点
      • 3、构建者模式与工厂模式的区别
    • 三、构建者模式在源码架构中的体现
      • 1、JDK的StringBuilder
      • 2、Tomcat中ServerEndpointConfig的构建
      • 3、MyBatis中SqlSessionFactoryBuilder
      • 4、Spring中也大量用到构建者模式
    • 四、要点回顾

一、前言

构建者模式和工厂模式一样,都是创建型设计模式,都是用来创建对象的。创建对象,无非就是通过无参或者有参构造函数new一个,工厂模式也是把这个new的动作封装起来,与客户端解耦,那为什么还要用构建者模式呢?

传统创建对象的弊端

对象简单,直接用构造函数是完全没有问题的,但对于复杂对象,有7、8+个属性要在实例化的时候指定,这时再用传统的方式可就不合适了:

  • 构造函数的参数列表会很长,代码可读性和易用性都非常差,客户端需要关心每个参数的含义以及顺序,容易传错。
  • 可以先用简单的构造函数实例化对象,然后用set设置或者修改对象里的属性值。这也会有问题,必填项无法强制调用者调用set,把必填项放到构造函数里,如果必填项多了,构造函数又面临参数列表过长的问题。

以上问题的解决就是构建者模式的应用场景。构建者模式,将复杂对象的创建过程与其表示分离,通过设置不同的可选参数,“定制化”创建不同的对象表示。

二、构建者模式

构建者模式是如何解决问题的呢?其核心思想就是:

  • 将复杂对象的创建过程(比如set属性值),委托给构建者去做
  • 最后通过构建者的构建方法,真正去创建对象。
  • 构建方法里调用需要实例化的类的构造函数,并将构建者的属性值复制给该对象。复制前可以做一些参数校验和默认值赋值等操作。

Builder类图

1、通用写法

类图很简单,代码也很简单。

  1. public class Product {
  2. private String s1;
  3. private String s2;
  4. private String s3;
  5. private Integer i1;
  6. private Integer i2;
  7. private Integer i3;
  8. private Product(Builder builder) {
  9. this.s1 = builder.s1;
  10. this.s2 = builder.s2;
  11. this.s3 = builder.s3;
  12. this.i1 = builder.i1;
  13. this.i2 = builder.i2;
  14. this.i3 = builder.i3;
  15. }
  16. // 省略getter 不提供setter
  17. @Override
  18. public String toString() {
  19. return "Product{" +
  20. "s1='" + s1 + '\'' +
  21. ", s2='" + s2 + '\'' +
  22. ", s3='" + s3 + '\'' +
  23. ", i1=" + i1 +
  24. ", i2=" + i2 +
  25. ", i3=" + i3 +
  26. '}';
  27. }
  28. public static class Builder {
  29. private String s1;
  30. private String s2;
  31. private String s3;
  32. private Integer i1;
  33. private Integer i2;
  34. private Integer i3;
  35. public static Builder create(String s1) {
  36. return new Builder(s1);
  37. }
  38. private Builder(String s1) {
  39. this.s1 = s1;
  40. }
  41. public Builder setS1(String s1) {
  42. this.s1 = s1;
  43. return this;
  44. }
  45. public Builder setS2(String s2) {
  46. this.s2 = s2;
  47. return this;
  48. }
  49. public Builder setS3(String s3) {
  50. this.s3 = s3;
  51. return this;
  52. }
  53. public Builder setI1(Integer i1) {
  54. this.i1 = i1;
  55. return this;
  56. }
  57. public Builder setI2(Integer i2) {
  58. this.i2 = i2;
  59. return this;
  60. }
  61. public Builder setI3(Integer i3) {
  62. this.i3 = i3;
  63. return this;
  64. }
  65. public Product build() {
  66. // 1. s1 必传
  67. if (s1 == null || s2 == "") {
  68. throw new IllegalArgumentException("s1必传,不可为空!");
  69. }
  70. // 2. s2 s3 非必填,但是具有依赖关系,即s2传了,s3也必须传
  71. if (s2 != null && s2 != "") {
  72. if (s3 == null || s3 == "") {
  73. throw new IllegalArgumentException("s2与s3具有依赖关系!");
  74. }
  75. }
  76. // 3. i1 不传或者 i1 <= 0 则默认为 1
  77. if (i1 == null || i1 <= 0) {
  78. i1 = 1;
  79. }
  80. // 4. i2非必传 但是需要校验有效性,不能小于0
  81. if (i2 != null && i2 < 0) {
  82. throw new IllegalArgumentException("i2 不能小于0");
  83. }
  84. Product product = new Product(this);
  85. return product;
  86. }
  87. }
  88. }
  89. public class Test {
  90. public static void main(String[] args) {
  91. Product product = Product.Builder.create("1")
  92. .setS2("2")
  93. .setS3("3")
  94. .setI1(0)
  95. .setI2(2)
  96. .setI3(3)
  97. .build();
  98. System.out.println(product.toString());
  99. }
  100. }
  101. // Product{s1='1', s2='2', s3='3', i1=1, i2=2, i3=3}

注意:

  • Builder可以是内部类,也可以是外部类,视情况而定,一般情况是作为产品类的内部类,这样产品类多了,不至于多出很多Builder类,让代码结构变得复杂。
  • Builder和产品类中代码重复,产品类的属性需要复制一份到Builder,并且要保持同步更新。
  • Builderset方法需要返回Builder本身,达到链式调用的效果。
  • Builderbuild()方法中实例化产品类,并且做一些必要的参数校验,非必填的参数校验也可以放到set里。
  • Product产品类的构造函数设置成private的,并且没有对外提供set方法,主要是为了创建产品类只能通过Builder,并且创建出来的对象是不可修改的。
  • Product的构造函数的参数为Builder,这样为了方便将值复制给Product,也可以不用这样做。
  • Product.Builder对外提供了一个静态方法create,这个方法可有可无,是将Builder的创建隔离,同时一些必填参数也可以通过Builder的构造函数传入。

2、构建者模式的优缺点

(1)优点:隔离了复杂对象的创建细节,方便客户端调用,减少了不必要的bug。

(2)缺点:代码重复,有一定的维护成本。

总结一句就是,方便他人,麻烦自己

3、构建者模式与工厂模式的区别

(1)工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象,一般这个创建过程也比较简单,如果复杂的话,可以工厂模式和构建者模式结合。

(2)构建者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”创建不同的对象

三、构建者模式在源码架构中的体现

1、JDK的StringBuilder

JDK中StringBuilder其实就是用了构建者模式,append拼接字符串,最后toString生成一个String对象。

  1. StringBuilder stringBuilder = new StringBuilder();
  2. stringBuilder.append("1").append("2").toString();

2、Tomcat中ServerEndpointConfig的构建

ServerEndpointConfig.Builder

3、MyBatis中SqlSessionFactoryBuilder

SqlSessionFactoryBuilder中只有多个build方法,并且参数直接从build中传入,这就是灵活应用,必填项有多种情况,那就对外提供多个不同参数列表的build,外界一看build方法就知道怎么调用。

SqlSessionFactoryBuilder

4、Spring中也大量用到构建者模式

(1)比如org.springframework.beans.factory.support.BeanDefinitionBuilderBeanDefinition中存在多个属性,但是Spring没有在BeanDefinitionBuilder再复制一遍,而是直接new一个BeanDefinitionBeanDefinitionBuilder,后面设置属性时,也是直接设置给了BeanDefinition对象,这样做的好处是减少重复代码,灵活使用构建者模式。

BeanDefinitionBuilder

(2)再比如org.springframework.messaging.support.MessageBuilder,这个构建者模式就用的比较中规中矩了:

MessageBuilder

MessageBuilder

四、要点回顾

构建者模式本身很简单,难的是活学活用,认识其本质,为什么那么设计?解决了什么问题?才能避免生搬硬套。

  1. 构建者模式,将复杂对象的创建过程与其表示分离,通过设置不同的可选参数,“定制化”创建不同的对象表示。
  2. 构建者模式主要解决了传统创建复杂对象(创建的过程较复杂),构造函数参数列表过长,无法做复杂校验等问题。
  3. 看了开源框架中对构建者模式的应用,并没有中规中矩,学到两点:如果必填参数的情况比较多,且每种情况参数个数较少,可以直接重载多个build方法,这样外界调用也非常直观;如果类对象属性较多,可以不必要在Builder里再把属性复制一份,而是在创建Builder同时创建产品对象,后面set值直接set给产品对象,build时再做一些参数校验。
  4. 对于一些中间对象,比如数据库映射,某个方法因参数过多封装成一个类,后面并不关心这个对象的无状态(可以随意set),即使属性较多,也没必要使用构建者模式。

参考:

  • 《设计模式之美》王争
  • Mybatis3源码
  • Spring源码
  • Tomcat10源码

如若文章有错误理解,欢迎批评指正,同时非常期待你的留言和点赞。如果觉得有用,不妨点个在看,让更多人受益。

发表评论

表情:
评论列表 (有 0 条评论,85人围观)

还没有评论,来说两句吧...

相关阅读

    相关 厚黑

    第2章 为人处世厚黑学 厚黑处事,要灵活,当伸则伸,当屈则屈,刚柔并济,左右逢源,要容人更要防人。 大智若愚,明哲保身。 遇事学会“推”,但要分情况,还要把握住火候。

    相关 构建builder模式

      遇到多个构造器参数时,我们可以考虑使用构建器Builder模式,相比于重叠构造器模式(定义多个重载的构造函数),JavaBeans模式(通过set方法挨个构造),Build