写点什么

03. 建造者模式设计思想

作者:杨充
  • 2024-10-21
    湖北
  • 本文字数:9560 字

    阅读完需:约 31 分钟

03.建造者模式设计思想

目录介绍

  • 01.建造者模式介绍

  • 1.1 建造者模式由来

  • 1.2 建造者模式定义

  • 1.3 建造者模式场景

  • 1.4 建造者模式思考

  • 02.建造者模式实现

  • 2.1 罗列一个场景

  • 2.2 创造对象弊端场景

  • 2.3 案例演变分析

  • 2.4 用例子理解建造者

  • 03.建造者模式分析

  • 3.1 建造者模式结构图

  • 3.2 建造者模式时序图

  • 3.3 基本代码实现

  • 04.建造者案例实践

  • 4.1 盖房子案例开发

  • 4.2 普通盖房子开发

  • 4.3 构造者优化盖房子

  • 05.建造者模式拓展

  • 5.1 建造者能简化吗

  • 5.2 和工厂模式区别

  • 06.建造者优缺点分析

  • 6.1 优点有哪些

  • 6.2 不足的点分析

  • 07.构造者模式总结

  • 7.1 该模式总结

  • 7.2 更多内容推荐

01.建造者模式基础介绍

1.0 AI 生成博客摘要

本文详细介绍了建造者模式的设计思想及其应用场景。主要内容包括建造者模式的由来、定义、适用场景及思考,通过实例讲解了如何使用建造者模式解决复杂对象的创建问题。文章还对比了建造者模式与工厂模式的区别,并分析了建造者模式的优缺点。最后,提供了多个相关资源链接,帮助读者深入理解和应用设计模式。

1.1 建造者模式由来

1.无论是在现实世界中还是在软件系统中,都存在一些复杂的对象,它们拥有多个组成部分。


如汽车,它包括车轮、方向盘、发送机等各种部件。而对于大多数用户而言,无须知道这些部件的装配细节,也几乎不会使用单独某个部件,而是使用一辆完整的汽车,可以通过建造者模式对其进行设计与描述。


建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节。


2.复杂对象相当于一辆有待建造的汽车,而对象的属性相当于汽车的部件,建造产品的过程就相当于组合部件的过程。


由于组合部件的过程很复杂,因此,这些部件的组合过程往往被“外部化”到一个称作建造者的对象里。


建造者返还给客户端的是一个已经建造完毕的完整产品对象,而用户无须关心该对象所包含的属性以及它们的组装方式,这就是建造者模式的模式动机。

1.2 建造者模式定义

建造者模式(Builder Pattern):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。


建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。


建造者模式属于对象创建型模式。根据中文翻译的不同,建造者模式又可以称为生成器模式。

1.3 建造者模式场景

在以下情况下可以使用建造者模式:


  1. 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。

  2. 需要生成的产品对象的属性相互依赖,需要指定其生成顺序。

  3. 对象的创建过程独立于创建该对象的类。在建造者模式中引入了指挥者类,将创建过程封装在指挥者类中,而不在建造者类中。

  4. 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。

1.4 建造者模式思考

建造者模式的原理和代码实现非常简单,掌握起来并不难,难点在于应用场景。


  1. 比如,你有没有考虑过这样几个问题:直接使用构造函数或者配合 set 方法就能创建对象,为什么还需要建造者模式来创建呢?

  2. 建造者模式和工厂模式都可以创建对象,那它们两个的区别在哪里呢?

02.建造者模式原理与实现

2.1 罗列一个场景

在平时的开发中,创建一个对象最常用的方式是,使用 new 关键字调用类的构造函数来完成。


我的问题是,什么情况下这种方式就不适用了,就需要采用建造者模式来创建对象呢?你可以先思考一下,下面我通过一个例子来带你看一下。


假设有这样一道设计面试题


我们需要定义一个资源池配置类 ResourcePoolConfig。这里的资源池,你可以简单理解为线程池、连接池、对象池等。在这个资源池配置类中,有以下几个成员变量,也就是可配置项。现在,请你编写代码实现这个 ResourcePoolConfig 类。

2.2 创造对象弊端场景

最常见、最容易想到的实现思路如下代码所示。


public class BuilderDesign1 {
public static void main(String[] args) { ResourcePoolConfig resourcePoolConfig = new ResourcePoolConfig("", 1, 2, 3); }
public static class ResourcePoolConfig { private static final int DEFAULT_MAX_TOTAL = 8; private static final int DEFAULT_MAX_IDLE = 8; private static final int DEFAULT_MIN_IDLE = 0;
private String name; private int maxTotal = DEFAULT_MAX_TOTAL; private int maxIdle = DEFAULT_MAX_IDLE; private int minIdle = DEFAULT_MIN_IDLE;
public ResourcePoolConfig(String name, int maxTotal, int maxIdle, int minIdle) { this.name = name; if (maxTotal <= 0) { throw new IllegalArgumentException("maxTotal should be positive."); } this.maxTotal = maxTotal; if (maxIdle < 0) { throw new IllegalArgumentException("maxIdle should not be negative."); } this.maxIdle = maxIdle; if (minIdle < 0) { throw new IllegalArgumentException("minIdle should not be negative."); } this.minIdle = minIdle; } //...省略getter方法... }}
复制代码


现在,ResourcePoolConfig 只有 4 个可配置项,对应到构造函数中,也只有 4 个参数,参数的个数不多。


但是,如果可配置项逐渐增多,变成了 8 个、10 个,甚至更多,那继续沿用现在的设计思路,构造函数的参数列表会变得很长,代码在可读性和易用性上都会变差。


在使用构造函数的时候,我们就容易搞错各参数的顺序,传递进错误的参数值,导致非常隐蔽的 bug。


// 参数太多,导致可读性差、参数可能传递错误ResourcePoolConfig config = new ResourcePoolConfig("dbconnectionpool", 16, null, 8, null, false , true, 10, 20,false, true);
复制代码

2.3 案例演变分析

解决这个问题的办法你应该也已经想到了,那就是用 set()函数来给成员变量赋值,以替代冗长的构造函数。


我们直接看代码,具体如下所示。其中,配置项 name 是必填的,所以我们把它放到构造函数中设置,强制创建类对象的时候就要填写。


其他配置项 maxTotal、maxIdle、minIdle 都不是必填的,所以我们通过 set() 函数来设置,让使用者自主选择填写或者不填写。


public static class ResourcePoolConfig {    private static final int DEFAULT_MAX_TOTAL = 8;    private static final int DEFAULT_MAX_IDLE = 8;    private static final int DEFAULT_MIN_IDLE = 0;
private String name; private int maxTotal = DEFAULT_MAX_TOTAL; private int maxIdle = DEFAULT_MAX_IDLE; private int minIdle = DEFAULT_MIN_IDLE;
public ResourcePoolConfig(String name) { this.name = name; }
public void setMaxTotal(int maxTotal) { if (maxTotal <= 0) { throw new IllegalArgumentException("maxTotal should be positive."); } this.maxTotal = maxTotal; }
public void setMaxIdle(int maxIdle) { if (maxIdle < 0) { throw new IllegalArgumentException("maxIdle should not be negative."); } this.maxIdle = maxIdle; }
public void setMinIdle(int minIdle) { if (minIdle < 0) { throw new IllegalArgumentException("minIdle should not be negative."); } this.minIdle = minIdle; } //...省略getter方法...}
复制代码


接下来,我们来看新的 ResourcePoolConfig 类该如何使用。我写了一个示例代码,如下所示。没有了冗长的函数调用和参数列表,代码在可读性和易用性上提高了很多。


// ResourcePoolConfig使用举例ResourcePoolConfig config = new ResourcePoolConfig("dbconnectionpool");config.setMaxTotal(16);config.setMaxIdle(8);
复制代码


至此,我们仍然没有用到建造者模式,通过构造函数设置必填项,通过 set() 方法设置可选配置项,就能实现我们的设计需求。


  1. set 方式设置对象属性时,存在中间状态,并且属性校验时有前后顺序约束,逻辑校验的代码找不到合适的地方放置。

  2. set 方法还破坏了"不可变对象"的密闭性。也就是说,对象在创建好之后,就不能再修改内部的属性值。要实现这个功能,我们就不能在 ResourcePoolConfig 类中暴露 set() 方法。

2.4 用例子理解建造者

可以把校验逻辑放置到 Builder 类中,先创建建造者,并且通过 set() 方法设置建造者的变量值,然后在使用 build() 方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象。


除此之外,我们把 ResourcePoolConfig 的构造函数改为 private 私有权限。这样我们就只能通过建造者来创建 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; } //...省略getter方法...
//我们将Builder类设计成了ResourcePoolConfig的内部类。 //我们也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。 public static class Builder { private static final int DEFAULT_MAX_TOTAL = 8; private static final int DEFAULT_MAX_IDLE = 8; private static final int DEFAULT_MIN_IDLE = 0;
private String name; private int maxTotal = DEFAULT_MAX_TOTAL; private int maxIdle = DEFAULT_MAX_IDLE; private int minIdle = DEFAULT_MIN_IDLE;
public ResourcePoolConfig build() { // 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等 if (StringUtils.isBlank(name)) { throw new IllegalArgumentException("..."); } if (maxIdle > maxTotal) { throw new IllegalArgumentException("..."); } if (minIdle > maxTotal || minIdle > maxIdle) { throw new IllegalArgumentException("..."); }
return new ResourcePoolConfig(this); }
public Builder setName(String name) { if (StringUtils.isBlank(name)) { throw new IllegalArgumentException("..."); } this.name = name; return this; }
public Builder setMaxTotal(int maxTotal) { if (maxTotal <= 0) { throw new IllegalArgumentException("..."); } this.maxTotal = maxTotal; return this; }
public Builder setMaxIdle(int maxIdle) { if (maxIdle < 0) { throw new IllegalArgumentException("..."); } this.maxIdle = maxIdle; return this; }
public Builder setMinIdle(int minIdle) { if (minIdle < 0) { throw new IllegalArgumentException("..."); } this.minIdle = minIdle; return this; } }}
// 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdleResourcePoolConfig config = new ResourcePoolConfig.Builder() .setName("dbconnectionpool") .setMaxTotal(16) .setMaxIdle(10) .setMinIdle(12) .build();
复制代码


实际上,使用建造者模式创建对象,还能避免对象存在无效状态。我再举个例子解释一下。


比如我们定义了一个长方形类,如果不使用建造者模式,采用先创建后 set 的方式,那就会导致在第一个 set 之后,对象处于无效状态。具体代码如下所示:


Rectangle r = new Rectange(); // r is invalidr.setWidth(2); // r is invalidr.setHeight(3); // r is valid
复制代码


为了避免这种无效状态的存在,我们就需要使用构造函数一次性初始化好所有的成员变量。


如果构造函数参数过多,我们就需要考虑使用建造者模式,先设置建造者的变量,然后再一次性地创建对象,让对象一直处于有效状态。


实际上,如果我们并不是很关心对象是否有短暂的无效状态,也不是太在意对象是否是可变的。比如,对象只是用来映射数据库读出来的数据,那我们直接暴露 set() 方法来设置类的成员变量值是完全没问题的。


而且,使用建造者模式来构建对象,代码实际上是有点重复的,ResourcePoolConfig 类中的成员变量,要在 Builder 类中重新再定义一遍。

03.建造者模式分析

3.1 建造者模式结构图

建造者模式包含如下角色:


  1. Builder:抽象建造者

  2. ConcreteBuilder:具体建造者

  3. Director:指挥者

  4. Product:产品角色

3.2 建造者模式时序图

建造者模式时序图如下所示:


04.建造者案例实践

4.1 盖房子案例开发

例如,让我们考虑如何创建一个 House(房屋)对象。


为了建造一个简单的房子,您需要建造四堵墙和一层地板,安装一扇门,安装一对窗户,并建造一座屋顶。但是,如果您想要一个更大、更明亮的房子,带有后院和其他设施(如供暖系统、管道和电气布线)呢?


最简单的解决方案是扩展基类 House 并创建一组子类来涵盖所有参数的组合。


但是,最终您将得到相当数量的子类。任何新的参数,如门廊风格,都将需要进一步扩展这个层次结构。建造者模式允许您逐步构建复杂的对象。

4.2 普通方式盖房子

使用普通方式盖房子,代码如下所示:


public class BuilderHouse {
public static void main(String[] args) { CommonHouse commonHouse = new CommonHouse(); commonHouse.build(); HeightBuilding heightBuilding = new HeightBuilding(); heightBuilding.build(); }
public static abstract class AbstractHouse {
/** * 打地基 */ public abstract void buildBasic();
/** * 砌墙 */ public abstract void buildWalls();
/** * 封顶 */ public abstract void roofed();
public void build() { buildBasic(); buildWalls(); roofed(); }
}
public static class CommonHouse extends AbstractHouse {
@Override public void buildBasic() { System.out.println(" 普通房子打地基 "); }
@Override public void buildWalls() { System.out.println(" 普通房子砌墙 "); }
@Override public void roofed() { System.out.println(" 普通房子封顶 "); }
}
public static class HeightBuilding extends AbstractHouse {
@Override public void buildBasic() { System.out.println(" 高楼打地基 "); }
@Override public void buildWalls() { System.out.println(" 高楼房子砌墙 "); }
@Override public void roofed() { System.out.println(" 高楼房子封顶 "); } }}
复制代码


分析


  1. 优点:比较好理解,简单易操作

  2. 缺点:程序结构过于简单,没有设计缓存层对象,程序的扩展和维护不好。这种设计方案,把产品(即:房子) 和 创建产品的过程(即:建房子流程) 封装在一起,耦合性增强了

  3. 改进:使用建造者模式,将产品和产品建造过程解耦

4.3 构造者优化盖房子

使用构建者模式实现房子的构建


private void test() {    ///盖普通房子    //准备创建房子的指挥者    HouseDirector houseDirector = new HouseDirector(new CommonHouse());    //完成盖房子,返回产品(普通房子)    House commonHouse = houseDirector.constructHouse();    System.out.println("普通房子:" + commonHouse.toString());    ///盖高楼    //重置建造者,改成修高楼    houseDirector.setHouseBuilder(new HighBuilding());    //完成盖房子,返回产品(高楼)    House highBuilding = houseDirector.constructHouse();    System.out.println("高楼:" + highBuilding.toString());}
/** * 产品->Product */public class House { private String basic; private String wall; private String roofed;
public String getBasic() { return basic; }
public void setBasic(String basic) { this.basic = basic; }
public String getWall() { return wall; }
public void setWall(String wall) { this.wall = wall; }
public String getRoofed() { return roofed; }
public void setRoofed(String roofed) { this.roofed = roofed; }
public House(String basic, String wall, String roofed) { this.basic = basic; this.wall = wall; this.roofed = roofed; }
public House() { }
@Override public String toString() { return "House{" + "basic='" + basic + '\'' + ", wall='" + wall + '\'' + ", roofed='" + roofed + '\'' + '}'; }}
/** * 抽象的建造者 */public abstract class HouseBuilder { /** * 组合House */ protected House house = new House();
//-------------------------将建造的流程写好--------------------------
/** * 打地基 */ public abstract void buildBasic();
/** * 砌墙 */ public abstract void buildWalls();
/** * 封顶 */ public abstract void roofed();
/** * 建造好房子后将产品(房子) 返回 * * @return */ public House buildHouse() { return house; }}

/** * 具体建造者 */public class CommonHouse extends HouseBuilder {
@Override public void buildBasic() { System.out.println("普通房子打地基5米 "); super.house.setBasic("地基5米"); }
@Override public void buildWalls() { System.out.println("普通房子砌墙10cm "); super.house.setWall("墙10cm"); }
@Override public void roofed() { System.out.println("普通房子屋顶 "); super.house.setRoofed("普通房子屋顶"); }
}
/** * 具体建造者 */public class HighBuilding extends HouseBuilder {
@Override public void buildBasic() { System.out.println("高楼的打地基100米 "); super.house.setBasic("地基100米"); }
@Override public void buildWalls() { System.out.println("高楼的砌墙20cm "); super.house.setWall("墙20cm"); }
@Override public void roofed() { System.out.println("高楼的透明屋顶 "); super.house.setRoofed("透明屋顶"); }}

/** * 指挥者,调用制作方法,返回产品 */public class HouseDirector { /** * 聚合 */ HouseBuilder houseBuilder = null;
/** * 方式一:构造器传入 houseBuilder * * @param houseBuilder */ public HouseDirector(HouseBuilder houseBuilder) { this.houseBuilder = houseBuilder; }
/** * 方式二:通过setter 传入 houseBuilder * * @param houseBuilder */ public void setHouseBuilder(HouseBuilder houseBuilder) { this.houseBuilder = houseBuilder; }
/** * 指挥者统一管理建造房子的流程 * * @return */ public House constructHouse() { houseBuilder.buildBasic(); houseBuilder.buildWalls(); houseBuilder.roofed(); return houseBuilder.buildHouse(); }}
复制代码

05.建造者模式拓展

5.1 建造者能简化吗

可以简化,建造者模式的简化:


  1. 省略抽象建造者角色:如果系统中只需要一个具体建造者的话,可以省略掉抽象建造者。

  2. 省略指挥者角色:在具体建造者只有一个的情况下,如果抽象建造者角色已经被省略掉,那么还可以省略指挥者角色,让 Builder 角色扮演指挥者与建造者双重角色。

5.2 和工厂模式区别

实际上,工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。


建造者模式是用来创建一种类型的复杂对象。通过设置不同的可选参数,“定制化”地创建不同的对象。


网上有一个经典的例子很好地解释了两者的区别。


顾客走进一家餐馆点餐,我们利用工厂模式,根据用户不同的选择,来制作不同的食物,比如披萨、汉堡、沙拉。对于披萨来说,用户又有各种配料可以定制,比如奶酪、西红柿、起司,我们通过建造者模式根据用户选择的不同配料来制作披萨。


也不要太学院派,非得把工厂模式建造者模式分得那么清楚,我们需要知道的是,每个模式为什么这么设计,能解决什么问题。只有了解了这些最本质的东西,我们才能不生搬硬套,才能灵活应用,甚至可以混用各种模式创造出新的模式,来解决特定场景的问题。

06.建造者优缺点分析

6.1 优点有哪些

建造者优点分析


  1. 在建造者模式中, 客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。

  2. 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者, 用户使用不同的具体建造者即可得到不同的产品对象 。

  3. 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。

  4. 增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合“开闭原则”。

6.2 不足的点分析

建造者缺点分析


  1. 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。

  2. 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。

07.构造者模式总结

7.1 该模式总结

Builder 模式有几个好处:


  1. Builder 的 setter 函数可以包含安全检查,可以确保构建过程的安全,有效。

  2. Builder 的 setter 函数是具名函数,有意义,相对于构造函数的一长串函数列表,更容易阅读维护。

  3. 可以利用单个 Builder 对象构建多个对象,Builder 的参数可以在创建对象的时候利用 setter 函数进行调整


当然 Builder 模式也有缺点:


  1. 更多的代码,需要 Builder 这样的内部类

  2. 增加了类创建的运行时开销,但是当一个类参数很多的时候,Builder 模式带来的好处要远胜于其缺点。

7.2 更多内容推荐


23 种设计模式



发布于: 刚刚阅读数: 5
用户头像

杨充

关注

还未添加个人签名 2018-07-30 加入

还未添加个人简介

评论

发布
暂无评论
03.建造者模式设计思想_杨充_InfoQ写作社区