学习设计模式——建造者模式

354 阅读7分钟

介绍

将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。建造者模式隐藏了复杂对象的创建过程,它把复杂对象的创建过程加以抽象,通过子类继承或者重载的方式,动态的创建具有复合属性的对象。

优缺点

优点:

  • 建造者独立,易扩展,有利于系统解耦。
  • 封装性好,构建和表示分离。
  • 客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险。

缺点:

  • 产品必须有共同点,范围有限制。
  • 如内部变化复杂,会有很多的建造类,后期维护成本较大。

结构:

  1. 产品角色(Product):它是包含多个组成部件的复杂对象,由具体建造者来创建其各个零部件。
  2. 抽象建造者(Builder):它是一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法。
  3. 具体建造者(Concrete Builder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。
  4. 指挥者(Director):它调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。 借用别人的图:

image.png 接下来我们通过一个实际例子来理解建造者模式。

实例

假设我们需要定义一个资源池配置类 ResourcePoolConfig。你可以简单理解为线程池、连接池等,在这个资源池配置类中,有以下几个变量,也就是可配置项。

成员变量解释是否必填
name资源名称
maxTotal最大总资源数量
maxIdle最大空闲资源数量
minIdle最小空闲资源数量

产品角色(ResourcePoolConfig)

/**
 * @Author:
 * @Description: 资源池配置(产品实现类)
 */
public class ResourcePoolConfig {

    private String name;  // 必填
    private int maxTotal;   // 选填
    private int maxIdle;    // 选填
    private int minIdle;    // 选填

    public ResourcePoolConfig(String name) {
        if(name == null || name == ""){
            throw new IllegalArgumentException("name should not be empty.");
        }
        this.name = name;
    }

    public void setMaxTotal(int maxTotal) {
        this.maxTotal = maxTotal;
    }

    public void setMaxIdle(int maxIdle) {
        this.maxIdle = maxIdle;
    }

    public void setMinIdle(int minIdle) {
        this.minIdle = minIdle;
    }

    @Override
    public String toString() {
        return "ResourcePoolConfig{" +
                "name='" + name + ''' +
                ", maxTotal=" + maxTotal +
                ", maxIdle=" + maxIdle +
                ", minIdle=" + minIdle +
                '}';
    }
}

抽象建造者(Builder)

/**
 * @Author:
 * @Description: 抽象建造者
 */
public abstract class Builder {

    public abstract void buildMaxTotal();

    public abstract void buildMaxIdle();

    public abstract void buildMinIdle();

    public abstract ResourcePoolConfig getResourcePoolConfig();

}

具体建造者(ThreadResourcePoolConfig、DBResourcePoolConfig)

/**
 * @Author:
 * @Description: 线程池连接配置类(具体建造者)
 */
public class ThreadResourcePoolConfig extends Builder{

    private ResourcePoolConfig resourcePoolConfig;

    public ThreadResourcePoolConfig(String name){
        resourcePoolConfig = new ResourcePoolConfig(name);
    }

    @Override
    public void buildMaxTotal() {
        resourcePoolConfig.setMaxTotal(8);
    }

    @Override
    public void buildMaxIdle() {
        resourcePoolConfig.setMaxIdle(8);
    }

    @Override
    public void buildMinIdle() {
        resourcePoolConfig.setMinIdle(1);
    }

    @Override
    public ResourcePoolConfig getResourcePoolConfig() {
        return resourcePoolConfig;
    }
}


/**
 * @Author:
 * @Description: 数据库连接池配置(具体建造者)
 */
public class DBResourcePoolConfig extends Builder {

    private ResourcePoolConfig resourcePoolConfig;

    public DBResourcePoolConfig(String name) {
        resourcePoolConfig = new ResourcePoolConfig(name);
    }

    @Override
    public void buildMaxTotal() {
        resourcePoolConfig.setMaxTotal(10);
    }

    @Override
    public void buildMaxIdle() {
        resourcePoolConfig.setMaxIdle(10);
    }

    @Override
    public void buildMinIdle() {
        resourcePoolConfig.setMinIdle(2);
    }

    @Override
    public ResourcePoolConfig getResourcePoolConfig() {
        return resourcePoolConfig;
    }
}

指挥者(ResourcePoolConfigDirector)

/**
 * @Author:
 * @Description: 指挥者
 */
public class ResourcePoolConfigDirector {

    private Builder builder;

    public ResourcePoolConfigDirector(Builder builder){
        this.builder = builder;
    }

    public ResourcePoolConfig build() {
        builder.buildMaxTotal();
        builder.buildMaxIdle();
        builder.buildMinIdle();
        return builder.getResourcePoolConfig();
    }

}

使用:

/**
 * @Author:
 * @Description: 客户端使用
 */
public class Client {

    public static void main(String[] args) {
        ThreadResourcePoolConfig threadconnectionpool = new ThreadResourcePoolConfig("threadconnectionpool");
        ResourcePoolConfig trpc = new ResourcePoolConfigDirector(threadconnectionpool).build();
        System.out.println(trpc.toString());

        DBResourcePoolConfig dbconnectionpool = new DBResourcePoolConfig("dbconnectionpool");
        ResourcePoolConfig drpc = new ResourcePoolConfigDirector(dbconnectionpool).build();
        System.out.println(drpc.toString());
    }

}

结果:

ResourcePoolConfig{name='threadconnectionpool', maxTotal=8, maxIdle=8, minIdle=1}
ResourcePoolConfig{name='dbconnectionpool', maxTotal=10, maxIdle=10, minIdle=2}

分析

上面的例子呐就是经典的建造者模式的实现流程,说实话,我觉得上面的例子用来解析经典的建造者模式并不合适,包括在网络上看到的许多例子也都不太合适,但也能够展示出大致的实现流程,对理解建造者模式也有一些帮助。

我觉得这篇文章的例子用来讲解建造者模式是比较合适的: 建造者模式应用场景_

其实建造者模式在Java中还有一种简化的使用方式。

另一种实现方式

当一个类的构造函数参数个数超过4个,而且这些参数有些是可选的参数,考虑使用构造者模式。

还是上面的例子,ResourcePoolConfig 只有 4 个可配置项,对应到构造函数中,也只有 4 个参数。但是,如果可配置项主键增多,变成了 8 个、10 个,那构造函数的参数列表会变得很长,代码在可读性和易用性上都会变差。在使用构造函数的时候,我们就容易搞错各参数的顺序,传递进错误的参数值,导致非常隐蔽的 bug。

ResourcePoolConfigDirector resourcePoolConfigDirector = new ResourcePoolConfigDirector("threadconnectionpool", null, 1, 3, 20, 50);

你也可能说可以用 set() 方法来设置配置项。

public class ResourcePoolConfig {

    private String name;  // 必填
    private int maxTotal;   // 选填
    private int maxIdle;    // 选填
    private int minIdle;    // 选填

    public setName(String name) {
        this.name = name;
    }

    public void setMaxTotal(int maxTotal) {
        this.maxTotal = maxTotal;
    }

    public void setMaxIdle(int maxIdle) {
        this.maxIdle = maxIdle;
    }

    public void setMinIdle(int minIdle) {
        this.minIdle = minIdle;
    }
}

这样仍然存在一些问题:

  • 如果我们把必填项也通过 set() 方法设置,那校验这些必填项是否已经填写的逻辑就无处安放了。
  • 除此之外,假设配置项之间有一定的依赖关系,比如,maxIdle 和 minIdle 要小于等于 maxTotal。如果我们继续使用现在的设计思路,那这些配置项之间的依赖关系或者约束条件的校验逻辑就无处安放了。
  • 如果我们希望 ResourcePoolConfig 类对象是不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值。要实现这个功能,我们就不能在 ResourcePoolConfig 类中暴露 set() 方法。

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

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.maxTotal;
        this.maxIdle = builder.maxIdle;
        this.minIdle = builder.minIdle;
    }

    public String getName() {
        return name;
    }

    public int getMaxTotal() {
        return maxTotal;
    }

    public int getMaxIdle() {
        return maxIdle;
    }

    public int getMinIdle() {
        return minIdle;
    }
    
   
    /**
     * 我们将Builder类设计成了ResourcePoolConfig的内部类
     * 也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder
     */
    public static class Builder {
        private String name;
        private int maxTotal = 8;
        private int maxIdle = 7;
        private int minIdle = 0;

        public ResourcePoolConfig build() {
            // 校验逻辑放到这里来做,包括必填项校验、约束条件校验等
            if(name == null || name == ""){
                throw new IllegalArgumentException("not empty");
            }
            if (maxIdle > maxTotal){
                throw new IllegalArgumentException("xxx");
            }
            if (minIdle > maxTotal || minIdle > maxIdle){
                throw new IllegalArgumentException("xxx");
            }
            return new ResourcePoolConfig(this);
        }

        public Builder setName(String name){
            this.name = name;
            return this;
        }

        public Builder setMaxTotal(int maxTotal){
            this.maxTotal = maxTotal;
            return this;
        }

        public Builder setMaxIdle(int maxIdle){
            this.maxIdle = maxIdle;
            return this;
        }

        public Builder setMinIdle(int minIdle){
            this.minIdle = minIdle;
            return this;
        }

    }

    @Override
    public String toString() {
        return "ResourcePoolConfig{" +
                "name='" + name + ''' +
                ", maxTotal=" + maxTotal +
                ", maxIdle=" + maxIdle +
                ", minIdle=" + minIdle +
                '}';
    }
}

使用:

public class Test {
    public static void main(String[] args) {
        ResourcePoolConfig config = new ResourcePoolConfig.Builder()
                .setName("dbconnectionpool")
                .setMaxTotal(20)
                .setMaxIdle(10)
                .setMinIdle(5)
                .build();
        System.out.println(config.toString());
    }
}

输出:

ResourcePoolConfig{name='dbconnectionpool', maxTotal=20, maxIdle=10, minIdle=5}

使用建造者模式创建对象,还能避免对象存在无效状态,如果我们采用先创建后 set 的方式,那就会导致在第一个 set 之后,对象处于无效状态。

People people = new People(); // people 是无效的
people.setName("小黑");   // people 是无效的
people.setAge("20");    // people 是有效的

使用建造者模式创建对象,就可以保证对象创建过程中一直处于有效状态。

与工厂模式的区别

工厂方法模式注重的是整体对象的创建方式;而建造者模式注重的是部件构建的过程,意在通过一步一步地精确构造创建出一个复杂的对象。

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