建造者模式,你还不懂?

123 阅读4分钟

建造者模式(Builder Design Pattern)

前面介绍了创建型设计模式的单例模式、工厂模式(工厂方法和抽象工厂),那么建造者模式应该在什么场景下使用呢?

日常编程或许遇到的疑惑

疑惑1:创建类时,类的构造函数参数很多

public class DataSourceConfig {
    private String url;
    private String username;
    private String password;
    // 连接池初始化连接数
    private int initialSize;
    // 连接池最小连接数
    private int minIdle;
    // 连接池最大连接数
    private int maxActive;
    // 连接等待超时的时间
    private int maxWait;

    public DataSourceConfig(String url, String username, String password, int initialSize, int minIdle, int maxActive, int maxWait) {
        ...
    }
    
    // getter method
}

上面的例子中,构造 DataSourceConfig 对象,使用的是全参的构造器,构造器的参数列表长度达到7。

函数的参数列表一般达到4个以上,我们可以认为参数列表比较长。使用参数列表较长的构造方法时,调用者要注意传参的顺序,很容易出错,而且,参数列表过长,要看清楚传参和参数对应关系十分费力,代码的可读性明显降低。

疑惑2:参数的初始化,部分是必须的,部分是可选的,应该提供怎样的方式构造对象

public class DataSourceConfig {
    // 默认初始化连接数
    public static final int DEFAULT_INITIAL_SiZE = 5;
    // 默认最小连接数
    public static final int DEFAULT_MIN_IDLE = 1;
    // 默认最大连接数
    public static final int DEFAULT_MAX_ACTIVE = 10;
    // 默认等待超时的时间
    public static final int DEFAULT_MAX_WAIT = 3000;

    // 必须
    private String url;
    private String username;
    private String password;
    
    // 可选的
    // 连接池初始化连接数
    private int initialSize = DEFAULT_INITIAL_SiZE;
    // 连接池最小连接数
    private int minIdle = DEFAULT_MIN_IDLE;
    // 连接池最大连接数
    private int maxActive = DEFAULT_MAX_ACTIVE;
    // 连接等待超时的时间
    private int maxWait = DEFAULT_MAX_WAIT;

    public DataSourceConfig(String url, String username, String password) {
        ...
    }
    
    // setter getter method
}

...
DataSourceConfig config = new DataSourceConfig(...);// invalid
config.setInitialSize(5);//  invalid
config.setMinIdle(1);//  invalid
config.setMaxActive(5);//  invalid
config.setMaxWait(5000);// valid

为了应对部分参数必须要设置、部分不需要的场景,上面的示例代码,在构造函数中初始化必须初始化的参数,然后在 setter 方法中设置可选初始化的参数。这样的处理方式有问题吗?答案是有问题的。

有什么问题呢?

  • 只要必须初始化数量不低于4(也不能肯定后续需求是否有调整),参数列表过长的问题依旧没有解决;
  • 如果参数间存在依赖或者约束(e.g. minIdle、maxActive 参数要保持 minIdle > maxActive 的关系),校验参数间的依赖约束关系是否合法,将无法编写,单独写一个方法做验证操作,又很容易漏调用;
  • 如果我们要求初始化完成的对象是不可变的,现在还是依旧对外暴露 setter 方法,使对象的状态不确定;
  • 最后,在上面的示例代码中,在完成对象 DataSourceConfig 构造后,执行到最终的 set 方法之前,对象都是无效状态的,如果对象已对外暴露,会带给程序不确定性;

等等问题...

建造者模式,是为了解决这些问题而出现。上面的例子通过建造者模式调整后,示例代码如下:

public class DataSourceConfig {
    private String url;
    private String username;
    private String password;
    
    // 连接池初始化连接数
    private int initialSize;
    // 连接池最小连接数
    private int minIdle;
    // 连接池最大连接数
    private int maxActive;
    // 连接等待超时的时间
    private int maxWait;

    private DataSourceConfig(Builder builder) {
        this.url = builder.url;
        this.username = builder.username;
        this.password = builder.password;
        this.initialSize = builder.initialSize;
        this.minIdle = builder.minIdle;
        this.maxActive = builder.maxActive;
        this.maxWait = builder.maxWait;
    }
    
    // getter method
    
    public static class Builder {
        // 默认初始化连接数
        public static final int DEFAULT_INITIAL_SiZE = 5;
        // 默认最小连接数
        public static final int DEFAULT_MIN_IDLE = 1;
        // 默认最大连接数
        public static final int DEFAULT_MAX_ACTIVE = 10;
        // 默认等待超时的时间
        public static final int DEFAULT_MAX_WAIT = 3000;
        
        private String url;
        private String username;
        private String password;
        private int initialSize = DEFAULT_INITIAL_SiZE;
        private int minIdle = DEFAULT_MIN_IDLE;
        private int maxActive = DEFAULT_MAX_ACTIVE;
        private int maxWait = DEFAULT_MAX_WAIT;
        
        public DataSourceConfig build() {
            // 可做参数校验逻辑,e.g. minIdle <= maxActive
            return new DataSourceConfig(this);
        }
        
        // setter 方法
    }
}

DataSourceConfig config = new DataSourceConfig.Builder()
    .setUrl("..")
    .setUsername("..")
    .setPassword("..")
    ...
    .build();

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

建造者模式将构造对象拆分成多个步骤,对于不同的复杂对象,用户可以依据所需定制化步骤。同时,也能简化复杂的构造函数。但是,该模式下,增加多个类,会稍微提高了代码的复杂度,降低了代码的可读性。