Java设计模式14:建造者模式

  • 2019 年 10 月 3 日
  • 笔记

什么是建造者模式

发现很多框架的源码使用了建造者模式,看了一下觉得挺实用的,就写篇文章学习一下,顺便分享给大家。

建造者模式是什么呢?用一句话概括就是建造者模式的目的是为了分离对象的属性与创建过程,是的,只要记住并理解红字的几个部分,建造者模式你就懂了。

 

为什么需要建造者模式

建造者模式是构造方法的一种替代方案,为什么需要建造者模式,我们可以想,假设有一个对象里面有20个属性:

  • 属性1
  • 属性2
  • 属性20

对开发者来说这不是疯了,也就是说我要去使用这个对象,我得去了解每个属性的含义,然后在构造函数或者Setter中一个一个去指定。更加复杂的场景是,这些属性之间是有关联的,比如属性1=A,那么属性2只能等于B/C/D,这样对于开发者来说更是增加了学习成本,开源产品这样的一个对象相信不会有太多开发者去使用。

为了解决以上的痛点,建造者模式应运而生,对象中属性多,但是通常重要的只有几个,因此建造者模式会让开发者指定一些比较重要的属性或者让开发者指定某几个对象类型,然后让建造者去实现复杂的构建对象的过程,这就是对象的属性与创建分离。这样对于开发者而言隐藏了复杂的对象构建细节,降低了学习成本,同时提升了代码的可复用性。

虽然感觉基本说清楚了,但还是有点理论,具体往下看一下例子。

 

建造者模式代码示例

举一个实际场景的例子:

大家知道一辆车是很复杂的,有发动机、变速器、轮胎、挡风玻璃、雨刮器、气缸、方向盘等等无数的部件。    用户买车的时候不可能一个一个去指定我要那种类型的变速器、我要一个多大的轮胎、我需要长宽高多少的车,这是不现实的    通常用户只会和销售谈我需要什么什么样的类型的车,马力要不要强劲、空间是否宽敞,这样销售就会根据用户的需要去推荐一款具体的车    这就是一个典型建造者的场景:车是复杂对象,销售是建造者。我告诉建造者我需要什么,建造者根据我的需求给我一个具体的对象

根据这个例子,我们定义一个简单的汽车对象:

 1 public class Car {   2   3     // 尺寸   4     private String size;   5   6     // 方向盘   7     private String steeringWheel;   8   9     // 底座  10     private String pedestal;  11  12     // 轮胎  13     private String wheel;  14  15     // 排量  16     private String displacement;  17  18     // 最大速度  19     private String maxSpeed;  20  21     public String getSize() {  22         return size;  23     }  24  25     public void setSize(String size) {  26         this.size = size;  27     }  28  29     public String getSteeringWheel() {  30         return steeringWheel;  31     }  32  33     public void setSteeringWheel(String steeringWheel) {  34         this.steeringWheel = steeringWheel;  35     }  36  37     public String getPedestal() {  38         return pedestal;  39     }  40  41     public void setPedestal(String pedestal) {  42         this.pedestal = pedestal;  43     }  44  45     public String getWheel() {  46         return wheel;  47     }  48  49     public void setWheel(String wheel) {  50         this.wheel = wheel;  51     }  52  53     public String getDisplacement() {  54         return displacement;  55     }  56  57     public void setDisplacement(String displacement) {  58         this.displacement = displacement;  59     }  60  61     public String getMaxSpeed() {  62         return maxSpeed;  63     }  64  65     public void setMaxSpeed(String maxSpeed) {  66         this.maxSpeed = maxSpeed;  67     }  68  69     @Override  70     public String toString() {  71         return "Car [size=" + size + ", steeringWheel=" + steeringWheel + ", pedestal=" + pedestal + ", wheel=" + wheel  72             + ", displacement=" + displacement + ", maxSpeed=" + maxSpeed + "]";  73     }  74  75 }

这里简单定义几个参数,然后建造者对象应运而生:

public class CarBuilder {        // 车型      private String type;        // 动力      private String power;        // 舒适性      private String comfort;        public Car build() {          Assert.assertNotNull(type);          Assert.assertNotNull(power);          Assert.assertNotNull(comfort);            return new Car(this);      }        public String getType() {          return type;      }        public CarBuilder type(String type) {          this.type = type;          return this;      }        public String getPower() {          return power;      }        public CarBuilder power(String power) {          this.power = power;          return this;      }        public String getComfort() {          return comfort;      }        public CarBuilder comfort(String comfort) {          this.comfort = comfort;          return this;      }        @Override      public String toString() {          return "CarBuilder [type=" + type + ", power=" + power + ", comfort=" + comfort + "]";      }    }

说是建造者,其实也不合适,它只是一个中间对象,用于接收来自外部的信息,比如需要什么样的车型,需要什么样的动力啊这些。

然后大家一定注意到了build方法,这个是建造者模式好像约定俗成的方法名,代表建造,里面把自身对象传给Car,这个构造方法的实现我在第一段代码里面是没有贴的,这段代码的实现为:

public Car(CarBuilder builder) {      if ("紧凑型车".equals(builder.getType())) {          this.size = "大小--紧凑型车";      } else if ("中型车".equals(builder.getType())) {          this.size = "大小--中型车";      } else {          this.size = "大小--其他";      }        if ("很舒适".equals(builder.getComfort())) {          this.steeringWheel = "方向盘--很舒适";          this.pedestal = "底座--很舒适";      } else if ("一般舒适".equals(builder.getComfort())) {          this.steeringWheel = "方向盘--一般舒适";          this.pedestal = "底座--一般舒适";      } else {          this.steeringWheel = "方向盘--其他";          this.pedestal = "底座--其他";      }        if ("动力强劲".equals(builder.getPower())) {          this.displacement = "排量--动力强劲";          this.maxSpeed = "最大速度--动力强劲";          this.steeringWheel = "轮胎--动力强劲";      } else if ("动力一般".equals(builder.getPower())) {          this.displacement = "排量--动力一般";          this.maxSpeed = "最大速度--动力一般";          this.steeringWheel = "轮胎--动力一般";      } else {          this.displacement = "排量--其他";          this.maxSpeed = "最大速度--其他";          this.steeringWheel = "轮胎--其他";      }  }

这是真实构建对象的地方,无论多复杂的逻辑都在这里实现而不需要暴露给开发者,还是那句核心的话:实现了对象的属性与构建的分离

这样用起来就很简单了:

@Test  public void test() {      Car car = new CarBuilder().comfort("很舒适").power("动力一般").type("紧凑型车").build();        System.out.println(JSON.toJSONString(car));  }

只需要指定我需要什么什么类型的车,然后具体的每个参数自然根据我的需求列出来了,不需要知道每个细节,我也能得到我需要的东西。

 

建造者模式在开源框架中的应用

文章的开头有说很多开源框架使用了建造者模式,典型的有Guava的Cache、ImmutableMap,不过感觉MyBatis更为大家熟知,且MyBatis内部大量使用了建造者模式,我们可以一起来看一下。

以原生的MyBatis(即不使用Spring框架进行整合)为例,通常使用MyBatis我们会用以下几句代码:

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

关键我们看就是这个SqlSessionFactoryBuilder,它的源码核心方法实现为:

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.        }      }    }      ...      public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {      try {        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);        return build(parser.parse());      } catch (Exception e) {        throw ExceptionFactory.wrapException("Error building SqlSession.", e);      } finally {        ErrorContext.instance().reset();        try {          inputStream.close();        } catch (IOException e) {          // Intentionally ignore. Prefer previous error.        }      }    }      ...    }

因为MyBatis内部是很复杂的,核心类Configuration属性多到爆炸,比如拿数据库连接池来说好了,有POOLED、UNPOOLED、JNDI三种,然后POOLED里面呢又有各种超时时间、连接池数量的设置,这一个一个都要让开发者去设置那简直要命了。因此MyBatis在SqlSessionFactory这一层使用了Builder模式,对开发者隐藏了XML文件解析细节,Configuration内部每个属性赋值细节,开发者只需要指定一些必要的参数(比如数据库地址、用户名密码之类的),就可以直接使用MyBatis了,至于可选参数,配置了就拿开发者配置的,没有配置就默认来一套。

通过这样一种方式,开发者接入MyBatis的成本被降到了最低,这么一种编程方式非常值得大家学习,尤其是自己需要写一些框架的时候。

同样的大家可以看一下Environment,Environment也使用了建造者模式,但是Environment使用建造者模式最大的作用是让用户无法在运行时修改任何环境属性保证了安全与稳定性,同样这也是建造者模式的一种经典实现。

 

建造者模式的类关系图

其实,建造者模式不像一些设计模式有比较固定或者比较类似的实现方式,它的核心只是分离对象属性与创建,整个实现比较自由,我们可以看到我自己写的造车的例子和SqlSessionFactoryBuilder就明显不是一种实现方式。

看了一些框架源码总结起来,建造者模式的实现大致有两种写法:

这是一种在Builder里面直接new对象的方式,MyBatis的SqlSessionFactoryBuilder就是这种写法,适用于属性之间关联不多且大量属性都有默认值的场景

另外一种就是间接new的方式了:

我的代码示例,还有例如Guava的Cache都是这种写法,适用于属性之间有一定关联性的场景,例如车的长宽高与轴距都属于车型一类、排量与马力都与性能相关,可以把某几个属性归类,然后让开发者指定大类即可。

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

 

建造者模式的优点及适用场景

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

  • 客户端不比知道产品内部细节,将产品本身与产品创建过程解耦,使得相同的创建过程可以创建不同的产品对象
  • 可以更加精细地控制产品的创建过程,将复杂对象分门别类抽出不同的类别来,使得开发者可以更加方便地得到想要的产品

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

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

总的来说,在IT这个行业,复杂的需求、复杂的业务逻辑层出不穷,这必然导致复杂的业务对象的增加,建造者模式是非常有用武之地的。合理分析场景,在合适的场景下使用建造者模式,一定会使得你的代码漂亮得多。