建造者(Builder)模式

1. 絮絮叨叨

  • 一说到如何新建一个对象,熟悉面向对象编程的同学,首先想到的就是new一个对象
  • 笔者在研究生期间,甚至听到过同学院的男同学这样调侃过找对象的事情:

    找对象太难了,僧多粥少,还可能找到个自己不满意的。要是能new一个“对象”多好啊,外貌、性格啥的都是自定义,保证满足要求。要是不满足要求,还能重新set一下属性

  • 当时笔者和朋友走在这几个男生的后面,顿时不厚道地大笑出声,虽然我也是单身狗,但这不妨碍我笑啊

1.1 通过构造函数创建对象

  • 创建对象,首先想到的就是通过构造函数创建对象,也就是new一个对象,并在new的时候传入初始值

  • 考虑具有5个属性的User类,其中name、telephone、sex都是必填字段,否则无法录入信息系统

    public class User {
        private String name; // 必填字段
        private String telephone; // 必填字段
        private String sex;  // 必填字段
        private int age; // 可选字段
        private String address; // 可选字段
    }
    
  • 对于可选字段,我们可以初始化这些字段中任意个,也可以不初始化

  • 这时,我们需要有多个构造函数,以创建具有不同状态的user对象

伸缩构造函数问题

  • 构造函数个数指数级增长
    • 想想在User类中,2个可选字段的存在,就搞得我们需要构建4个构造函数
    • 如果有 n n n个可选字段的存在,就需要 2 n 2^n 2n个构造函数
  • 构造函数的参数难以对应,容易传参错误
    • 构造函数中的参数一旦变多,到底哪个属性对应哪个参数,需要人工确认
    • 同时,还相邻的参数还可能类型相同,这就更加容易传错了
    • 遇到这样的问题,debug将变得非常困难。因为有些字段的内容可能没有明确的含义,如数值字段,很难判断哪个位置传参了错误
  • 那种感觉之酸爽,真的只有经历过的人才知道 😣
  • 国外的博客,将这种现象描述为伸缩构造函数问题(telescoping constructors problem,中文翻译可能缺少那么点儿味道)

1.2 JavaBean模式构建对象

  • 针对伸缩构造函数问题,可以通过提供setter方法设置对象属性值去解决问题
  • 也就是JavaBean模式,该模式也存在一些问题:
    • 有些字段是必填字段,如果在构建时忘记设置值,后面的调用者必须进行Preconditions以保证对象正确初始化
    • 同时,因为多要多次调用setter方法设置属性值,构造过程中JavaBean可能处于不一致的状态
      • 联系单例的初始化,已经成功创建的对象,还需要通过setter方法初始化(之前的博客:层层递进,实现单例模式
      • 这样的话,不同线程获得的单例对象状态是不一致的
      public static Singleton getInstance() {
          if (instance == null) { // 第一重检验,避免不必要的同步
              synchronized (Singleton.class) { // 同步锁
                  if (instance == null) { // 第二重检验,避免重复创建
                      instance = new Singleton(); // 至此,单例对象已成功创建,但其内容未初始化
                      // ...,通过setter方法设置属性值
                  }
              }
          }
          return instance;
      }
      
    • setter方法的开放,彻底打破了构建不可变类的可能

知识拓展: 不可变类(Immutable Class


2. 神奇的Builder模式

  • 针对上面的问题,可以使用建造者(Builder)模式,分离对象的属性与创建过程

  • 如果有幸阅读一些开源代码,你会经常碰到Builder模式,只是不知道这就是Builde者模式而已

    RuleIndex ruleIndex = RuleIndex.builder()
            .register(projectRule1)
            .register(projectRule2)
            .register(filterRule)
            .register(anyRule)
            .build();
    
  • 通过上面的代码示例,我们不难发现

    • 如果使用构造函数创建对象,必须通过参数传入属性值,属性与创建过程是绑定在一起的
    • 如果使用Builder模式,建造者自动实现复杂的对象创建过程。调用者只需要指定一些重要属性,便可以得到一个创建好的对象
    • 对于调用者来说,在Builder模式中,对象的属性与创建过程分离了

2.1 使用Builder模式重构User类

2.1.1 代码实现

  • 简化后的Builder模式重写User类:

    import com.google.common.base.Preconditions;
    import org.apache.commons.lang.StringUtils;
    
    public class User {
        private String name; // 必填字段
        private String telephone; // 必填字段
        private String sex; // 必填字段
        private int age; // 可选字段
        private String address; // 可选字段
    
        private User(UserBuilder builder) {
            this.name = builder.name;
            this.telephone = builder.telephone;
            this.sex = builder.sex;
            this.age = builder.age;
            this.address = builder.address;
        }
    
        @Override
        public String toString() {
          return "{\"name\":" + name + ",\"telephone\":\"" + telephone
                    + "\",\"sex\":\"" + sex + "\",\"age\":\""
                    + age + "\",\"address\":" + address + "}";
        }
    
        public static class UserBuilder {
            private String name;
            private String telephone;
            private String sex;
            private int age;
            private String address;
    
            public UserBuilder setName(String name) {
                this.name = name;
                return this;
            }
    
            public UserBuilder setTelephone(String telephone) {
                this.telephone = telephone;
                return this;
            }
    
            public UserBuilder setSex(String sex) {
                this.sex = sex;
                return this;
            }
    
            public UserBuilder setAge(int age) {
                this.age = age;
                return this;
            }
    
            public UserBuilder setAddress(String address) {
                this.address = address;
                return this;
            }
    
            public User build() {
                // 必填字段校验
                Preconditions.checkArgument(StringUtils.isNotBlank(name), "name can not be empty");
                Preconditions.checkArgument(StringUtils.isNotBlank(telephone), "name can not be empty");
                Preconditions.checkArgument(StringUtils.isNotBlank(telephone), "name can not be empty");
                // 传入builder对象以构建user对象
                User user = new User(this);
                System.out.println("通过Builder模式成功构建User对象");
                return user;
            }
        }
    }
    
  • 在main方法中,通过Builder模式创建User对象

    public static void main(String[] args) {
        User user = new User.UserBuilder()
                .setName("张三")
                .setTelephone("18812546788")
                .setSex("男")
                .setAge(31)
                .build();
        System.out.println("通过Builder模式构建的User对象: " + user);
    }
    
  • 执行结果如下:
    图片描述

2.1.2 总结与反思

(1)简化版的Builder模式总结
  1. 在User类中创建一个静态内部类Builder,该Builder类复制了User类的所有属性
  2. User类中创,创建一个以Builder对象为参数的private构造函数,从而可以通过Builder对象初始化User对象
  3. Builder类中,提供了设置属性的setter方法
    (1)与返回void的常见setter方法不同,Builder类中的setter方法会返回Builder类型的对象
    (2)这样可以基于Builder对象不断进行属性设置,还可以灵活地调整构建过程(谁先谁后,是否设置某个属性)
    (3)写到这里,忽然有点明白下面这句话的含义了

    将复杂对象的构建拆分成多个简单对象的构建,通过调整构建步骤可以创建具有不同表示的对象。

  4. Builder类中,创建一个build()方法:先进行必填字段的校验,然后调用User的private构造函数,完成user对象的创建并返回
(2)为什么User类中的构造函数是private?
  • 最开始,通过Builder模式重构User类时,自己将User类的构造函数设置为public权限
  • 后续学习过程,阅读的Builder模式源码变多后,发现目标类的构造函数都是private权限
  • 假设使用public权限,可以跳过build()方法,直接将未设置任何属性的Builder对象作为User类构造函数的入参。
  • 最终,创建的user对象将不符合要求,因为必填字段未初始化
    User user = new User(new User.UserBuilder());
    
  • 设置为private权限,保证只能通过Builder类的build()方法完成合法的use对象的创建
(3)关于Builder作为静态内部类的说明
  • 除了可以将Builder类作为静态内部类,还可以将Buidler类抽象成一个外部类
  • 例如,mybatis的SqlSessionFactoryBuilder、Google LocalCache中的CacheBuilder,都是外部类

2.2 静态的builder()创建Builder对象

  • 细心的读者可能会发现,main方法中通过Builder模式构建user对象的代码,与示例的开源代码有一些出入

  • 开源代码直接通过RuleIndex.builder()方法创建Builder对象,而非通过new User.UserBuilder()方式

  • 其实,builder()方法是一个静态方法,其内部实现就是创建一个Builder对象并返回

  • 基于User类示例,增加一个静态的builder()方法。main方法中,通过Builder模式构建user对象的代码便可以与示例的开源代码一致

    public static UserBuilder builder(){
        return new UserBuilder();
    }
    public static void main(String[] args) {
        User user = User.builder()
                .setName("张三")
                .setTelephone("18812546788")
                .setSex("男")
                .setAge(31)
                .build();
        System.out.println("通过Builder模式构建的user对象: " + user);
    }
    

build()方法 vs builder()方法

  • 位置不同:build()方法位于Builder类中,builder()方法位于User类中
  • 作用不同:build()方法,用于创建user对象;builder()方法,用于创建builder对象,与new User.UserBuilder()方式等价

2.3 另一种对象创建方式

  • 到目前为止,示例代码都是通过传入Builder对象实现目标对象的构建

  • 其实,在很多开源代码中,还有另一种创建目标对象的方法:直接传入Builder对象的属性值,以创建目标对象

  • 例如,Google guava的ImmutableList.Builder类,其build()方法如下。

  • 它并未将Builder对象作为ImmutableList类的构造函数入参,而是传入具体的属性值实现创建

    @Override
    public ImmutableList<E> build() {
      forceCopy = true;
      return asImmutableList(contents, size);
    }
    static <E> ImmutableList<E> asImmutableList(Object[] elements, int length) {
      switch (length) {
        case 0:
          return of();
        case 1:
          return of((E) elements[0]);
        default:
          if (length < elements.length) {
            elements = Arrays.copyOf(elements, length);
          }
          return new RegularImmutableList<E>(elements);
      }
    }
    

一些说明

  • 其他博客的理解:适用于属性之间关联不多且大量属性都有默认值的场景。
  • 自己的理解:
    • 这样的创建方式,适合需要通过Builder对象初始化的属性较少的情况
    • 反过来思考:如果属性较少,Builder模式其实显得没有意义,完全可以使用构造函数创建对象
    • 更准确地说,这样的创建方式,适合属性类型为数组或容器类的情况
    • 这样可以通过类似put、add等方法,不断向容器中添加元素
  • 两种创建方式对比如下:
    在这里插入图片描述

应用场景

  • 应用一:JDK中StringBuilder和StringBuffer,就是通过append方法不断向存储字符串的char[]中添加字符,然后通过toString()方法直接创建一个String对象

  • 应用二:MyBatis的SqlSessionFactoryBuilder类,其关键代码如下:

    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        SqlSessionFactory var5;
        try {
            // 构建XMLConfigBuilder对象
            XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); 
            // 调用XMLConfigBuilder对象的parse()方法构建Configuration对象
            var5 = this.build(parser.parse());
        } catch (Exception var14) {
            throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
        } finally {
            ErrorContext.instance().reset();
    
            try {
                inputStream.close();
            } catch (IOException var13) {
            }
    
        }
    
        return var5;
    }
    // 最终调用如下build方法,实现SqlSessionFactory的构建
    public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config); // config对象直接作为构造函数的入参
    }
    


3. 传统的Builder模式

3.1 UML图

  • 上面的Builder模式是改良过的版本,传统的Builder模式更加复杂
    • 产品(Product):包含多个组成部件(属性)的复杂对象,是Builder最终要创建的目标对象
    • 抽象建造者(Builder):一般为抽象类,定义了创建产品各组成部件的抽象方法,即设置对象属性的抽象方法);通常还包含一个返回产品的getResult()方法,对应改良版本的build()方法
    • 具体创建者(Concrete Builder):继承抽象创建者,实现创建产品各组成部件的抽象方法;也就是说,具体创建者负责创建具体的产品
    • 指挥者(Director):包含一个contruct()方法,通过调用Builder中创建对象各组成部件的方法和getResult()方法,实现复杂对象的创建。对应改良版本中,main()方法通过Builder创建产品的代码

3.2 传统Builder模式编程实战

  • 需求:

    • 电脑的几大关键组成部分:CPU、内存、显示器、磁盘、键盘、鼠标
    • 不同型号的电脑,其配置也有所差异
    • 可以通过Builder模式,将电脑的组成部件和创建过程分离
  • 定义电脑类,对应模式中的产品

    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.serializer.SerializerFeature;
    
    public class Computer {
        private String cpu;
        private String memory;
        private String disk;
        private String display;
        private String keyboard;
        private String mouse;
        // 省略getter和setter方法
    
        @Override
        public String toString() {
            return JSON.toJSONString(this, SerializerFeature.WRITE_MAP_NULL_FEATURES);
        }
    }
    
  • 定义抽象构建者类

    public abstract class ComputerBuilder {
        // 创建产品对象
        protected Computer computer = new Computer();
        // 创建产品对象的各组成部件,即设置对象的属性
        public abstract ComputerBuilder setCpu();
        public abstract ComputerBuilder setMemory();
        public abstract ComputerBuilder setDisk();
        public abstract ComputerBuilder setDisplay();
        public abstract ComputerBuilder setKeyboard();
        public abstract ComputerBuilder setMouse();
        // 返回产品对象
        public Computer getComputer(){
            return computer;
        }
    }
    
  • 定义两个具体构建者类,分别用于构建家用电脑和实验室电脑

    public class GeneralComputerBuilder extends ComputerBuilder {
        @Override
        public ComputerBuilder setCpu() {
            computer.setCpu("intel i5");
            return this;
        }
    
        @Override
        public ComputerBuilder setMemory() {
            computer.setMemory("8GB");
            return this;
        }
    
        @Override
        public ComputerBuilder setDisk() {
            computer.setDisk("1TB");
            return this;
        }
    
        @Override
        public ComputerBuilder setDisplay() {
            computer.setDisplay("联想显示器");
            return this;
        }
    
        @Override
        public ComputerBuilder setKeyboard() {
            computer.setKeyboard("戴尔键盘");
            return this;
        }
    
        @Override
        public ComputerBuilder setMouse() {
            computer.setMouse("戴尔鼠标");
            return this;
    	}
    }
    
    public class LabComputerBuilder extends ComputerBuilder{
        @Override
        public ComputerBuilder setCpu() {
            computer.setCpu("intel gold");
            return this;
        }
    
        @Override
        public ComputerBuilder setMemory() {
            computer.setMemory("16GB");
            return this;
        }
    
        @Override
        public ComputerBuilder setDisk() {
            computer.setDisk("2TB");
            return this;
        }
    
        @Override
        public ComputerBuilder setDisplay() {
            computer.setDisplay("三星2k曲面屏");
            return this;
        }
    
        @Override
        public ComputerBuilder setKeyboard() {
            computer.setKeyboard("罗技游戏键盘");
            return this;
        }
    
        @Override
        public ComputerBuilder setMouse() {
            computer.setMouse("罗技无线蓝牙鼠标");
            return this;
        }
    }
    
  • 定义指挥者,通过构建者类实现电脑的创建

    public class Director {
        private ComputerBuilder builder;
    
        public Director(ComputerBuilder builder) {
            this.builder = builder;
        }
    
        public Computer construct() {
            // 调用builder的属性设置方法
            builder.setCpu();
            builder.setMemory();
            builder.setDisk();
            builder.setDisplay();
            builder.setKeyboard();
            builder.setMouse();
            // 返回生产好的电脑
            return builder.getComputer();
        }
    }
    
  • 在main方法中,使用Builder模式创建电脑

    public static void main(String[] args) {
        GeneralComputerBuilder builder = new GeneralComputerBuilder();
        Director director = new Director(builder);
        Computer computer = director.construct();
        // 查看电脑信息
        System.out.println(computer.getClass().getSimpleName() + ": " + computer);
    }
    

3.3 回头看:Builder模式

Builder模式的定义

  • 《设计模式》一书对Buidler模式的定义:
    • 将复杂对象的创建过程与它的表示分离,使得同样的创建步骤可以创建出不同的表示

      自己的理解:对象的创建与属性的设置是分离的

    • Builer模式以正确的动作顺序,一步一步创建复杂对象
    • 指挥者掌控对象的创建过程,且只需要知道待创建对象的类型
  • Builder就像一个Fluent Interface,可以像类似lambda表达式一样,以方法级联或者方法链的方式创建对象

    这是Builder模式改良后的效果

Builder模式 vs 工厂模式

  • 二者都用于创建对象,它们者最大的区别:Builder模式为对象的创建过程提供了更多的控制权

    其实,就是提供了设置对象属性的权力,而非什么都不能控制,最终获得一个不满意的产品

  • 工厂模式专注于What:创建一个指定类型的固定对象,Builder专注于How:以什么样的步骤创建同一类型、但表示(属性)不同的对象

    In one sentence, abstract factory pattern is the answer to “WHAT” and the builder pattern to “HOW”.

改良版本的优缺点

  • 优点
    • 很好地解决了使用构造函数创建对象时的伸缩构造函数问题,以及使用Java Bean模式创建对象时的状态不一致、破坏不可变特性的问题
    • 对象的创建过程更加灵活:ImmutableList.Builder不同的add顺序,最终创建ImmutableList也会有所不同;可以选择性的初始化对象的某些属性,而非所有属性
    • 在笔者看来,相对通过构造函数创建对象,代码可读性更高。起码能将属性和值关联起来,或者能清晰地知道对象的内容(如ImmutableList)
  • 缺点
    • 一旦目标类的属性发生变化,Builder类也需要同步修改

传统版本的优缺点(自己仍需好好理解)

  • 优点
    • 封装性好,实现了对象的创建与表示分离
    • 扩展性好,具体建造者之间相互独立,有利于系统的解耦
    • 客户端无需知道产品的内部组成细节,建造者可以创建具有个性化的产品,而不影响其他模块
  • 缺点:
    • 产品的组成部分必须相同,也就说Builder模式只能生产具有不同细节的同一类产品
    • 如果产品的组成部件发生变化,建造者也要同步修改,不易进行代码维护

4. 从源码学习Builder模式

4.1 OkHttpClient

4.1.1 OkHttpClient中Builder模式的实现逻辑

  • OkHttpClient是典型的Builder模式,使用Builder对象作为其构造函数的入参

    public OkHttpClient() {
      this(new Builder());  
    }
    
    OkHttpClient(Builder builder) {
      this.dispatcher = builder.dispatcher;
      this.proxy = builder.proxy;
      ... // 省略其他初始代码,OkHttpClient属性的初始化,都是由Builder中的属性值决定
    }
    
  • 其中,Builder构造函数为OkHttpClient的各种属性赋予了默认值

    public Builder() {
      dispatcher = new Dispatcher();
      ... // 其他属性省略,只展示部分属性的默认值
      connectTimeout = 10_000;
      readTimeout = 10_000;
      writeTimeout = 10_000;
      pingInterval = 0;
    }
    
  • Builder类支持通过以属性名为方法名的setter方法,例如readTimeout()可以设置readTimeout属性的值,并返回Builder对象

    public Builder readTimeout(long timeout, TimeUnit unit) {
      readTimeout = checkDuration("timeout", timeout, unit);
      return this;
    }
    
  • Builder类的build()方法,直接调用OkHttpClient(Builder builder)实现OkHttpClient的创建

    public OkHttpClient build() {
      return new OkHttpClient(this);
    }
    

4.1.2 如何创建OkHttpClient?

  • 方法一: 通过new OkHttpClient.Builder()创建自定义属性的OkHttpClient

    private final OkHttpClient httpClient = new OkHttpClient.Builder()
               .addInterceptor(userAgent(DRIVER_NAME + "/" + DRIVER_VERSION))
               .readTimeout(5_000, TimeUnit.MILLISECONDS)
               .socketFactory(new SocketChannelSocketFactory())
               .build();
    
  • 方法二: 通过new OkHttpClient()创建具有默认属性值的OkHttpClient

    public final OkHttpClient client = new OkHttpClient();
    

5. 菜鸟的总结

  • 说实话,看了这么Builder模式的源码,还从未见过使用复杂的传统Builder模式的
  • 改良的Builder模式的实现方式大同小异
    • Builder对象的创建:直接创建,通过静态的builder()方法创建
    • 目标对象的创建(特指new那一步):
      • 以Builder对象作为构造函数的入参,如OkHttpClient
      • 或者以Builder对象的属性、或者处理后的属性作为构造函数的入参
  • 自己对Builder模式的理解:
    • 工厂模式中,client获得的是已经创建好的、具有默认值的对象;Builder模式中,client可以掌控对象的构建过程(例如,集中添加元素时,谁先谁后;创建复杂对象时,是否设置某些属性等),从而获得已经创建好的、具有自定义值的对象
    • Buidler模式解决了通过构造函数、Java Bean模式创建对象存在的问题
    • 一句话来说,虽然实现Builder模式增加了开发工作量,但是client可以轻松获得已经创建好的、具有自定义属性的对象
  • Builder模式在JDK源码、各种类库或开源组件的应用场景很多,但自己好像没办法跟工作中的问题联系起来
  • 可能是自己的理解不够透彻,也可能是工作中尚未出现需要使用Builder模式的场景😂

参考链接:

  • 5
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值