跳至主要內容

建造者模式

AruNi_Lu设计模式设计模式与范式约 2653 字大约 9 分钟

本文内容

1. 为什么需要建造者模式?

在开发时,我们创建一个对象常用的方式就是 new,new 一个对象是通过构造函数来完成的。那什么情况下不适合用构造函数来创建对象呢

假设现在需要设计一个 资源池配置类 ResourcePoolConfig(类比线程池、连接池等,资源可以复用,用完后归还即可),里面有如下几个变量,有些是必填项、有些有默认值(可填可不填):

image-20230403180436515

这个类的设计主要是要考虑如何处理非必填变量,一个比较简单的方法是提供一个全参构造器,规定传进来的变量如果为 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 (name.isBlank()) {
            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 be negative.");
            }
            this.maxIdle = maxIdle;
        }

        if (minIdle != null) {
            if (minIdle < 0) {
                throw new IllegalArgumentException("minIdle should be negative.");
            }
            this.minIdle = minIdle;
        }
    }
    
    // getter 略
}

这样,就设计完成了。但是如果 ResourcePoolConfig 类的 可配置参数变得更多,这样就会使得构造函数得参数列表变得很长,影响代码的可读性和易用性。在使用这个构造函数时就得非常小心的填写参数,导致非常隐蔽的 bug 出现,就像下面这样:

ResourcePoolConfig config = 
    new ResourcePoolConfig("dbconnectionpool", 16, null, 8, null, false , true, 10, 20falsetrue);

想解决参数列表过长的问题,其实提供 setter 方法就可以解决。我们让构造函数的参数列表只包含必须填写的变量,而其他非必填的变量,给用户暴露 set 方法进行注入,可以让用户自行选择设不设置。我们参数的校验逻辑也要移动到相应的 setter 方法中去。

使用方式如下:

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

一切都看起来很完美,但是,需求总是在不断变化的,如果我们还有如下要求:

  • 必填项变多了,此时构造函数的参数列表还是会出现过长的情况。如果把必填项也通过 set 方法注入的话,那怎么知道这些必填项到底有没有全部赋值呢?比如用户忘记调用了几个必填项的 set 方法。
  • 配置项之间存在依赖关系,如果现在规定,用户设置了非必填变量的其中一个,就必须也要设置另外两个;或者规定 maxIdle 必须要小于等于 maxTotal(本来就要考虑的)。这时这些依赖关系的检验逻辑代码应该放在哪里呢?
  • 把类对象改为是不可变对象,如果我们希望 ResourcePoolConfig 类对象在创建好后,就不能在修改内部的属性值了,此时我们就不能暴露 setter 方法了。

建造者模式 就可以很好地解决上述几个要求。

2. 什么是建造者模式?

建造者模式 的定义非常抽象,也没什么用,但是还是需要了解一下,它的定义是:将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示

个人觉得可以把 “表示” 理解为不同的参数、变量之间的依赖关系等。

现在可以丢掉上面这个抽象的定义了,我们来看看如何使用建造者模式解决上面的需求。

我们可以 新定义一个 Builder 类,把校验逻辑都放到 Builder 类中,先创建建造者,并通过 setter 方法设置建造者的变量值,然后再使用 build() 方法真正创建对象,在 build() 方法中做必填项的检验、依赖关系的检验等

接着,我们需要 把 ResourcePoolConfig 类的构造函数私有化,这样就只能通过建造者来创建类对象。而且 ResourcePoolConfig 类不要提供任何 setter 方法,这样创建出来的对象就无法被修改了(不可变对象)。

使用建造者模式后,具体实现如下:

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.maxIdle;
        this.maxIdle = builder.maxIdle;
        this.minIdle = builder.minIdle;
    }
    
    // getter 方法略,不提供 setter 方法

    /**
     * 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 (name.isBlank()) {
                throw new IllegalArgumentException("name should not be empty.");
            }
            if (maxIdle > maxTotal) {
                throw new IllegalArgumentException("maxIdle should less equals maxTotal.");
            }
            if (minIdle > maxTotal || minIdle > maxIdle) {
                throw new IllegalArgumentException("minIdle should less equals maxIdle and maxTotal.");
            }
            return new ResourcePoolConfig(this);
        }

        // 以下是 Builder 对外提供的 setter 方法
        public Builder setName(String name) {
            if (name.isBlank()) {
                throw new IllegalArgumentException("name should not be empty.");
            }
            this.name = name;
            return this;
        }

        public Builder setMaxTotal(int maxTotal) {
            if (maxTotal <= 0) {
                throw new IllegalArgumentException("maxTotal should be positive.");
            }
            this.maxTotal = maxTotal;
            return this;
        }

        public Builder setMaxIdle(int maxIdle) {
            if (maxIdle < 0) {
                throw new IllegalArgumentException("maxIdle should be negative.");
            }
            this.maxIdle = maxIdle;
            return this;
        }

        public Builder setMinIdle(int minIdle) {
            if (minIdle < 0) {
                throw new IllegalArgumentException("minIdle should be negative.");
            }
            this.minIdle = minIdle;
            return this;
        }
    }
}

使用示例如下:

public class Usage {
    public static void main(String[] args) {
        // 抛出 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

这里的无效状态会有什么影响呢?如果在并发场景下,不一次性设置好所有变量的值,那么线程就有可能获取到无效状态的对象。

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

不过,建造者模式有一个很明显的 缺点,那就是 代码有点重复,从上面的例子可以看出,ResourcePoolConfig 类中的成员变量,要在 Builder 类中又重新再定义一遍。所以,如果你不在意上面所提到的点,那么就可以使用原始方式,直接暴露 setter 方法让外部设置变量值来创建对象即可。

3. 与工厂模式有何区别?

建造者模式与工厂模式都是用来负责创建对象的工作,它们有什么区别呢?

实际上,工厂模式是用来创建类型相关,但创建出来的对象是不同的(继承同一父类的不同子类对象),由给定的参数来决定创建哪种具体的对象。而 建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化” 地创建不同的对象(注意这里类的类型是相同的)。

一个很典型的例子就是:客户到 KFC 点餐,利用工厂模式,可以根据客户不同的选择,来制作不同的食物,比如汉堡、披萨、炸鸡(这些都可以看作是 Food 类的子类)。而对于披萨来说,客户又有各种配料可以定制,比如奶酪、西红柿、起司等,通过建造者模式,可以根据用户选择的不同配料来制作披萨(虽然都是披萨(类型相同),但制作出来的披萨是不同的(对象不同))。

4. 总结

建造者模式的原理和实现都是比较简单的,重要的是需要掌握它的应用场景。

如果类中有很多属性,避免构造函数参数列表过长,影响代码的可读性和易用性,可以先考虑使用 set 方法解决。但是,如果还有如下需求,那么就可以考虑建造者模式了

  • 必填项变多了,此时构造函数的参数列表还是会出现过长的情况。如果把必填项也通过 set 方法注入的话,那么就不知道这些必填项到底有没有全部赋值
  • 配置项之间存在依赖关系,如果现在规定,用户设置了非必填变量的其中一个,就必须也要设置另外两个;或者规定某属性必须要小于等于另一属性等。这时这些依赖关系的检验逻辑代码就没地方可放了。
  • 把类对象改为是不可变对象,如果我们希望类对象在创建好后,就不能在修改内部的属性值了,此时我们就不能暴露 setter 方法了。

如果 需要保证对象一直处于有效状态,就只能使用构造函数创建对象,而不能一步一步的调用 set 方法,当使用构造函数参数列表过长时,也可以考虑使用建造者模式。

此外,还讲到了工厂模式和建造者模式的区别,具体区别如下:

  • 工厂模式是用来创建类型相关,但创建出来的对象是不同的(继承同一父类的不同子类对象),由给定的参数来决定创建哪种具体的对象
  • 建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化” 地创建不同的对象(注意这里类的类型是相同的)。
上次编辑于: