对象构造神器,建造者模式实操分享

2022-12-02 20:05:28 浏览数 (1)

一、介绍

建造者模式,简单的说,就是将对象的属性与创建分离,使得同样的构建过程可以创建不同的对象

初次接触这个概念,可能有点闷逼,下面我们来举例,你就好懂了!

假设有一个对象里面有20个属性,如果我要使用这个对象,我们可能需要把这20个属性弄明白,然后在构造函数或者创建一个对象通过set一个一个去指定,显然这对开发者来说非常吃力!

  • 属性1
  • 属性2
  • ......
  • 属性19
  • 属性20

通常一个对象中,重要的属性只有几个,如果我们要使用这个对象时,只需要指定一些比较重要的属性,其他属性由一个中间类来帮我们完成复杂的创建过程,是不是会轻松很多?这就是对象的属性与创建分离!也就是我们所要介绍的建造者设计模式。

说了这么多,感觉还是有些理论,我们看一下具体的例子。

二、示例

我们都知道,台式机由很多零部件组成,但是不同的用途可能所需的零部件不一样,比如用途:游戏电脑,可能对cpu、显卡要求很高;办公电脑,可能就没有那么高要求,不太需要配置声卡;家庭影音电脑,要求也不是很高,但是各个配件都要齐全等等。

我们就以组装电脑为例,来看一下怎么实现对象的属性与创建分离

  • 首先,我们创建一个实体类,定义好属性,内容如下:
代码语言:javascript复制
/**
 * 实体类
 * 电脑
 */
public class Computer {

    /**CPU*/
    private String cpu;

    /**显卡*/
    private String graphicsCard;

    /**硬盘*/
    private String hardDisk;

    /**内存*/
    private String memory;

    /**主板*/
    private String mainBoard;

    /**声卡*/
    private String soundCard;

    //... 省略 setter 和 getter
 
    @Override
    public String toString() {
        return "Computer [cpu = "   cpu
                  ",graphicsCard = "   graphicsCard
                  ",hardDisk = "   hardDisk
                  ",memory = "   memory
                  ",mainBoard = "   mainBoard
                  ",soundCard = "   soundCard   "]";
    }
}
  • 再创建一个建造类,也就是负责组装电脑,内容如下:
代码语言:javascript复制
/**
 * 建造者类
 */
public class ComputerBuilder {

    /**用途*/
    private String type;

    /**是否配置声卡,默认是*/
    private boolean isSoundCard = true;

    /**额外增加的内存条(单位G)*/
    private Integer addMemory;

    public String getType() {
        return type;
    }

    public ComputerBuilder type(String type) {
        this.type = type;
        return this;
    }

    public boolean isSoundCard() {
        return isSoundCard;
    }

    public ComputerBuilder setSoundCard(boolean soundCard) {
        isSoundCard = soundCard;
        return this;
    }

    public Integer getAddMemory() {
        return addMemory;
    }

    public ComputerBuilder addMemory(Integer addMemory) {
        this.addMemory = addMemory;
        return this;
    }

    /**创建Computer对象*/
    public Computer builder(){
        return new Computer(this);
    }
}
  • 其中一个关键就是builder()方法,里面使用Computer类的构造方法来创建对象,因此我们在Computer类中重载一个构造方法,内容如下:
代码语言:javascript复制
/**
 * 实体类
 * 电脑
 */
public class Computer {

    /**CPU*/
    private String cpu;

    /**显卡*/
    private String graphicsCard;

    /**硬盘*/
    private String hardDisk;

    /**内存*/
    private String memory;

    /**主板*/
    private String mainBoard;

    /**声卡*/
    private String soundCard;

    ... 省略 setter 和 getter


    public Computer(ComputerBuilder builder) {
        //按照用途不同,进行配置
        if("游戏配置".equals(builder.getType())){
            this.cpu = "Intel i7 处理器";
            this.graphicsCard = "英伟达显卡";
            this.hardDisk = "500G 机械硬盘";
            this.mainBoard = "华硕 主板";
        }else if("办公配置".equals(builder.getType())){
            this.cpu = "Intel i5 处理器";
            this.graphicsCard = "英伟达显卡";
            this.hardDisk = "200G 固态硬盘";
            this.mainBoard = "华硕 主板";
        }else if("家庭影音配置".equals(builder.getType())){
            this.cpu = "Intel i3 处理器";
            this.graphicsCard = "英伟达显卡";
            this.hardDisk = "500G 机械硬盘";
            this.mainBoard = "华硕 主板";
        }

        //判断是否需要配置声卡
        if(builder.isSoundCard()){
            this.soundCard = "英特尔声卡";
        }else{
            this.soundCard = "不配置声卡";
        }
        //默认4G 内存
        this.memory = (4   builder.getAddMemory())   "g 内存";
    }

    @Override
    public String toString() {
        return "Computer [cpu = "   cpu
                  ",graphicsCard = "   graphicsCard
                  ",hardDisk = "   hardDisk
                  ",memory = "   memory
                  ",mainBoard = "   mainBoard
                  ",soundCard = "   soundCard   "]";
    }
}
  • 最后,编写一个客户端,测试一下,内容如下:
代码语言:javascript复制
public class BuilderClient {

    public static void main(String[] args) {
        //创建一个办公配置的组装电脑
        Computer computer = new ComputerBuilder()
                .type("办公配置")//指定配置
                .addMemory(4)//增加4G 内存
                .setSoundCard(false)//不配置声卡
                .builder();
        //打印结果
        System.out.println(computer.toString());
    }
}

输出结果:

代码语言:javascript复制
Computer [cpu = Intel i5 处理器,graphicsCard = 英伟达显卡,hardDisk = 200G 固态硬盘,memory = 8g 内存,mainBoard = 华硕 主板,soundCard = 不配置声卡]

如果我们想要使用游戏配置,只需在type()方法里传入游戏配置,即可帮我们创建相应对口的组装电脑,使用者不需要关心具体创建过程,只需要指定关键属性类型就可以了,从而实现对象的属性与构建的分离!

三、应用

建造者设计模式,使用非常广泛,尤其是在开源框架中,比如我们最熟悉的 Mybatis 框架,获取SqlSessionFactory就是一个建造者模式的场景实际应用。

在使用 Mybatis (不使用 Spring 框架进行整合)的时候,我们会这样打开一个SqlSessionFactory,内容如下:

代码语言:javascript复制
// MyBatis配置文件路径
String resources = "mybatis_config.xml";
// 获取一个输入流
Reader reader = Resources.getResourceAsReader(resources);
// 获取SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
// 打开一个会话
SqlSession sqlSession = sqlSessionFactory.openSession();
// 具体操作
...

我们继续来看看SqlSessionFactoryBuilder类的源码,内容如下:

代码语言:javascript复制
public class SqlSessionFactoryBuilder {

  ...
  
  public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        reader.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }
  .....
  /**最后通过Configuration 创建一个SqlSessionFactory */
  public SqlSessionFactory build(Configuration config) {
      return new DefaultSqlSessionFactory(config);
  }
}

实际的Configuration类的属性非常多,部分内容如下:

代码语言:javascript复制
/**
 * @author Clinton Begin
 */
public class Configuration {

  protected Environment environment;

  protected boolean safeRowBoundsEnabled = false;
  protected boolean safeResultHandlerEnabled = true;
  protected boolean mapUnderscoreToCamelCase = false;
  protected boolean aggressiveLazyLoading = true;
  protected boolean multipleResultSetsEnabled = true;
  protected boolean useGeneratedKeys = false;
  protected boolean useColumnLabel = true;
  protected boolean cacheEnabled = true;
  protected boolean callSettersOnNulls = false;
  protected String logPrefix;
  protected Class <? extends Log> logImpl;
  protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
  protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
  protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" }));
  protected Integer defaultStatementTimeout;
  protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
  protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;

  .....
}

很明显,Mybatis通过配置文件获取的配置属性,解析完数据源配置信息之后,通过SqlSessionFactoryBuilder创建一个SqlSessionFactory对象,因为数据源配置特别多,不少于40个,这么多的配置,如果让开发者在代码中一个一个的去set,估计都要疯狂了,改用配置文件形式去配置属性,工作量就会少很多!

四、总结

建造者模式不像一些设计模式有比较固定或者比较类似的实现方式,它的核心只是分离对象属性与创建,整个实现比较自由。

总的来说,建造者模式的实现大致有两种写法:

  • 第一种:通过建造者直接 new 一个对象,用获取的对象然后进行属性赋值
  • 第二种:建造者通过构造方法创建一个对象,在构造方法里进行属性赋值

总体而言,两种没有太大的优劣之分,在合适的场景下选择合适的写法就好了。

但是,建造者模式这种设计模式,优缺点比较明显。从优点来说:

  • 客户端不必知道产品内部细节,将产品本身与产品创建过程解耦,使得相同的创建过程可以创建不同的产品对象,建造者独立,易扩展;
  • 可以更加精细地控制产品的创建过程,将复杂对象分门别类抽出不同的类别来,便于控制细节风险;

建造者模式说不上缺点,只能说这种设计模式的使用比较受限:

  • 产品属性之间差异很大且属性没有默认值可以指定,这种情况是没法使用建造者模式的,我们可以试想,一个对象20个属性,彼此之间毫无关联且每个都需要手动指定,那么很显然,即使使用了建造者模式也是毫无作用;

在IT这个行业,复杂的需求、复杂的业务逻辑层出不穷,合理分析场景,在合适的场景下使用建造者模式,一定会起到事半功倍的效果!

五、参考

博客园-五月的仓颉-建造者模式

0 人点赞