42、建造者模式

1、为什么需要建造者模式

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

  • 创建对象的属性有很多
  • 配置项之间有依赖关系、约束关系
  • 需要创建的对象为不可变对象

1.1、示例

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

成员变量 解释 是否必填 默认值
name 资源名称 没有
maxTotal 最大总资源数量 8
maxldle 最大空闲资源数量 8
minldle 最小空闲资源数量 0

只要你稍微有点开发经验,那实现这样一个类对你来说并不是件难事,最常见、最容易想到的实现思路如下代码所示
因为 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);

1.2、解决

解决这个问题的办法你应该也已经想到了,那就是用 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);

1.3、问题

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

  • 我们刚刚讲到,name 是必填的,所以我们把它放到构造函数中,强制创建对象的时候就设置
    如果必填的配置项有很多,把这些必填配置项都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题
    如果我们把必填项也通过 set() 方法设置,那校验这些必填项是否已经填写的逻辑就无处安放了
  • 假设配置项之间有一定的依赖关系,比如:如果用户设置了 maxTotal、maxIdle、minIdle 其中一个,就必须显式地设置另外两个
    或者配置项之间有一定的约束条件,比如:maxIdle 和 minIdle 要小于等于 maxTotal
    如果我们继续使用现在的设计思路,那这些配置项之间的依赖关系或者约束条件的校验逻辑就无处安放了
  • 如果我们希望 ResourcePoolConfig 类对象是不可变对象
    也就是说,对象在创建好之后,就不能再修改内部的属性值
    要实现这个功能,我们就不能在 ResourcePoolConfig 类中暴露 set() 方法

1.4、建造者模式

我们可以把校验逻辑放置到 Builder 类中,先创建建造者,并且通过 set() 方法设置建造者的变量值
然后在使用 build() 方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象

除此之外,我们把 ResourcePoolConfig 的构造函数改为 private 私有权限,这样我们就只能通过建造者来创建 ResourcePoolConfig 类对象
并且 ResourcePoolConfig 没有提供任何 set() 方法,这样我们创建出来的对象就是不可变对象了

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

public class ResourcePoolConfig {

    private String name;
    private int maxTotal;
    private int maxIdle;
    private int minIdle;
    // ... 省略 getter 方法 ...

    // 私有构造器, 创建不可变对象
    private ResourcePoolConfig(Builder builder) {
        this.name = builder.name;
        this.maxTotal = builder.maxTotal;
        this.maxIdle = builder.maxIdle;
        this.minIdle = builder.minIdle;
    }

    // 我们将 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
Builder builder = new ResourcePoolConfig.Builder();
ResourcePoolConfig config = builder.setName("dbconnectionpool")
                                   .setMaxTotal(16)
                                   .setMaxIdle(10)
                                   .setMinIdle(12)
                                   .build();

1.5、更多

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

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

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

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

2、与工厂模式有何区别

工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象
建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,"定制化" 地创建不同的对象

顾客走进一家餐馆点餐,我们利用工厂模式,根据用户不同的选择,来制作不同的食物,比如披萨、汉堡、沙拉
对于披萨来说,用户又有各种配料可以定制,比如奶酪、西红柿、起司,我们通过建造者模式根据用户选择的不同配料来制作披萨

posted @ 2023-06-26 15:20  lidongdongdong~  阅读(6)  评论(0编辑  收藏  举报