建造者模式
什么是建造者模式
建造者模式用一句话概括就是建造者模式的目的是为了分离对象的属性与创建过程,是的,只要记住并理解红字的几个部分,建造者模式你就懂了。
为什么需要建造者模式
建造者模式是构造方法的一种替代方案,为什么需要建造者模式,我们可以想,假设有一个对象里面有20个属性:
- 属性1
- 属性2
- ...
- 属性20
对开发者来说这不是疯了,也就是说我要去使用这个对象,我得去了解每个属性的含义,然后在构造函数或者Setter中一个一个去指定。更加复杂的场景是,这些属性之间是有关联的,比如属性1=A,那么属性2只能等于B/C/D,这样对于开发者来说更是增加了学习成本,开源产品这样的一个对象相信不会有太多开发者去使用。
为了解决以上的痛点,建造者模式应运而生,对象中属性多,但是通常重要的只有几个,因此建造者模式会让开发者指定一些比较重要的属性或者让开发者指定某几个对象类型,然后让建造者去实现复杂的构建对象的过程,这就是对象的属性与创建分离。这样对于开发者而言隐藏了复杂的对象构建细节,降低了学习成本,同时提升了代码的可复用性。
模式的定义与特点
定义:指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。
该模式的主要优点如下:
- 封装性好,构建和表示分离。
- 扩展性好,各个具体的建造者相互独立,有利于系统的解耦。
- 客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险。
其缺点如下:
- 产品的组成部分必须相同,这限制了其使用范围。
- 如果产品的内部变化复杂,如果产品内部发生变化,则建造者也要同步修改,后期维护成本较大。
建造者(Builder)模式和工厂模式的关注点不同:建造者模式注重零部件的组装过程,而工厂方法模式更注重零部件的创建过程,但两者可以结合使用。
模式的结构与实现
建造者(Builder)模式由产品、抽象建造者、具体建造者、指挥者等 4 个要素构成,现在我们来分析其基本结构和实现方法。
1. 模式的结构
建造者(Builder)模式的主要角色如下。
- 产品角色(Product):它是包含多个组成部件的复杂对象,由具体建造者来创建其各个零部件。
- 抽象建造者(Builder):它是一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法 getResult()。
- 具体建造者(Concrete Builder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。
- 指挥者(Director):它调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。
建造者模式代码示例
简单的汽车对象:
![94f1c2563554cc06e232bfcb19474043.gif](https://i-blog.csdnimg.cn/blog_migrate/41f10e67f33f18739cf53dce2e5bee90.gif)
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 @Override70 public String toString() {71 return "Car [size=" + size + ", steeringWheel=" + steeringWheel + ", pedestal=" + pedestal + ", wheel=" + wheel72 + ", displacement=" + displacement + ", maxSpeed=" + maxSpeed + "]";73 }74 75 }
建造者对象应运而生:
![0dc6c436ccb54e3382d8a9e5dff93951.gif](https://i-blog.csdnimg.cn/blog_migrate/4d6e0230dbd28822a3b0f0b5da3b25bb.gif)
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 + "]"; }}
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 = "轮胎--其他"; }}
这是真实构建对象的地方,无论多复杂的逻辑都在这里实现而不需要暴露给开发者,还是那句核心的话:实现了对象的属性与构建的分离。
这样用起来就很简单了:
@Testpublic void test() { Car car = new CarBuilder().comfort("很舒适").power("动力一般").type("紧凑型车").build(); System.out.println(JSON.toJSONString(car));}
只需要指定我需要什么什么类型的车,然后具体的每个参数自然根据我的需求列出来了,不需要知道每个细节,我也能得到我需要的东西。
建造者模式在开源框架中的应用
文章的开头有说很多开源框架使用了建造者模式,典型的有Guava的Cache、ImmutableMap,不过感觉MyBatis更为大家熟知,且MyBatis内部大量使用了建造者模式,我们可以一起来看一下。
以原生的MyBatis(即不使用Spring框架进行整合)为例,通常使用MyBatis我们会用以下几句代码:
![8d9dd1a32a5bda106f25ddaa46f0721a.gif](https://i-blog.csdnimg.cn/blog_migrate/7e14e36a4debfaee7f2ea09e1864efef.gif)
// MyBatis配置文件路径String resources = "mybatis_config.xml";// 获取一个输入流Reader reader = Resources.getResourceAsReader(resources);// 获取SqlSessionFactorySqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);// 打开一个会话SqlSession sqlSession = sqlSessionFactory.openSession();// 具体操作...
![79366b5725b31178f6a9d847c6b28d3c.gif](https://i-blog.csdnimg.cn/blog_migrate/f6c39a6d8af82485492ce526a5f81852.gif)
关键我们看就是这个SqlSessionFactoryBuilder,它的源码核心方法实现为:
![7c7a34be5dc5d5f12748190fbe4b0f93.gif](https://i-blog.csdnimg.cn/blog_migrate/40464de6459a9948e925defe9e4365bb.gif)
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个属性,彼此之间毫无关联且每个都需要手动指定,那么很显然,即使使用了建造者模式也是毫无作用
结构型模式概述
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。
由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。
结构型模式分为以下 7 种:
- 代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
- 适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
- 桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现的,从而降低了抽象和实现这两个可变维度的耦合度。
- 装饰(Decorator)模式:动态地给对象增加一些职责,即增加其额外的功能。
- 外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
- 享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
- 组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
以上 7 种结构型模式,除了适配器模式分为类结构型模式和对象结构型模式两种,其他的全部属于对象结构型模式,下面我们会分别、详细地介绍它们的特点、结构与应用。