Java设计模式 — 建造者模式

什么是建造者模式?

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

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

为什么需要建造者模式?

建造者模式是构造方法的一种替代方案,为什么需要建造者模式,我们可以想,假设有一个对象里面有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都是这种写法,适用于属性之间有一定关联性的场景,例如车的长宽高与轴距都属于车型一类、排量与马力都与性能相关,可以把某几个属性归类,然后让开发者指定大类即可。

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

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

1.客户端不比知道产品内部细节,将产品本身与产品创建过程解耦,使得相同的创建过程可以创建不同的产品对象
2.可以更加精细地控制产品的创建过程,将复杂对象分门别类抽出不同的类别来,使得开发者可以更加方便地得到想要的产品想了想,说缺点,建造者模式说不上缺点,只能说这种设计模式的使用比较受限。
3.产品属性之间差异很大且属性没有默认值可以指定,这种情况是没法使用建造者模式的,我们可以试想,一个对象20个属性,彼此之间毫无关联且每个都需要手动指定,那么很显然,即使使用了建造者模式也是毫无作用

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容