1. 概述
生成器模式是一种创建型设计模式, 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象。
在我看来,五种创建型模式,可以分为三种:原型模式、单例模式、其他三种创建型模式。这样分是因为原型和单例的特点和适用场景是很明显的,而其他三种创建型模式的特点和适用场景则很相似。
其他三种创建型模式,工厂模式、抽象工厂模式、建造者模式,工厂模式和抽象工厂自然不必多说,抽象工厂是工厂模式的变种,这里可以看我之前的博客。那工厂模式和建造者模式有什么区别呢?
-
相同点:两者都是用于构建复杂对象实例的设计模式
-
不同点:工厂模式不关心细节,它造出来的对象是一个完整的对象,它对于该对象的细节并不是很关心;建造者则恰恰相反,它很关心细节,它关心对象是怎么一步一步构建的。
工厂方法模式注重的是整体对象的创建方法,而建造者模式注重的是部件构建的过程,旨在通过一步一步地精确构造创建出一个复杂的对象。我们举个简单例子来说明两者的差异,如要制造一个超人,如果使用工厂方法模式,直接产生出来的就是一个力大无穷、能够飞翔、内裤外穿的超人;而如果使用建造者模式,则需要组装手、头、脚、躯干等部分,然后再把内裤外穿,才能创造一个超人。
下面将来细细讲下建造者模式,还是从问题引入。例如, 我们来思考如何创建一个 房屋House
对象。 建造一栋简单的房屋, 首先你需要建造四面墙和地板, 安装房门和一套窗户, 然后再建造一个屋顶。 但是如果你想要一栋更宽敞更明亮的房屋, 还要有院子和其他设施 (例如暖气、 排水和供电设备), 那又该怎么办呢?两种解决方法:
-
最简单的方法是扩展房屋基类, 然后创建一系列涵盖所有参数组合的子类。每多出一个需求的房子,建一个新的子类,但最终你将面对相当数量的子类。 任何新增的参数 (例如门廊类型) 都会让这个层次结构更加复杂。
-
另一种方法则无需生成子类。 你可以在房屋基类中创建一个包括所有可能参数的超级构造函数, 并用它来控制房屋对象。 这种方法确实可以避免生成子类, 但它却会造成另外一个问题。
通常情况下, 绝大部分的参数都没有使用,这些参数也不是每次都要全部用上的。 这使得对于构造函数的调用十分不简洁。 例如, 只有很少的房子有游泳池, 因此与游泳池相关的参数十之八九是毫无用处的。
综上,这两种解决方法都不能解决问题,那怎么解决呢?就是下文的建造者模式了。这里再提一嘴工厂模式,上面讲它的区别时可能有点抽象。
工厂模式关心的种类的差别比较大,建造者模式关心的种类的差别比较小,对于当前建房子的场景,使用工厂模式要解决的就是建普通房子和建别墅的问题,使用建造者模式要解决的就是建一扇门两扇窗带游泳池的平房和建4扇门不带窗没游泳池平房的问题。
为什么说工厂模式不关心细节呢?因为在他眼里只有别墅和平房两种,平房有什么细节他不管,它才不管平房要多少门多少窗带不带游泳池,它构建出来的对象都是一样的,也就是说工厂模式建好后的平房都是一样的。讲到这里大家应该就明白了。
总结时间到:
- 建造者模式其实也是工厂模式的变种(我看网上好像没有这种说法,这个是我的个人理解),它关心粒度更小的场景。
- 以工厂模式为基础,抽象工厂模式为了解决更高维度的扩展,建造者模式为了解决更小粒度的扩展。
- 工厂模式,关注的是创建的产品,即创建什么类型的产品;建造者模式,关注的是产品的创建过程,即怎么创建。我们说 Builder 模式是将对象的构建与表示相分离。或许我们可以这样理解:Builder 模式侧重的是构建,而工厂模式侧重的是表示。
2. 特点
-
优点
- 可以分步创建对象, 暂缓创建步骤或递归运行创建步骤。
- 生成不同形式的产品时, 你可以复用相同的制造代码。
- 单一职责原则。 你可以将复杂构造代码从产品的业务逻辑中分离出来。
-
缺点
- 由于该模式需要新增多个类, 因此代码整体复杂程度会有所增加
-
使用场景
-
使用生成器模式可避免 “重叠构造函数 (telescopic constructor)” 的出现,一般来说超过4个参数且有可选参数时就可以考虑使用建造者模式
假设你的构造函数中有十个可选参数, 那么调用该函数会非常不方便; 因此, 你需要重载这个构造函数, 新建几个只有较少参数的简化版。 但这些构造函数仍需调用主构造函数, 传递一些默认数值来替代省略掉的参数。
生成器模式让你可以分步骤生成对象, 而且允许你仅使用必须的步骤。 应用该模式后, 你再也不需要将几十个参数塞进构造函数里了。 -
当你希望使用代码创建不同形式的产品 (例如石头或木头房屋) 时, 可使用生成器模式。
如果你需要创建的各种形式的产品, 它们的制造过程相似且仅有细节上的差异, 此时可使用生成器模式。
基本生成器接口中定义了所有可能的制造步骤, 具体生成器将实现这些步骤来制造特定形式的产品。 同时, 主管类将负责管理制造步骤的顺序。
-
使用生成器构造组合树或其他复杂对象。
生成器模式让你能分步骤构造产品。 你可以延迟执行某些步骤而不会影响最终产品。 你甚至可以递归调用这些步骤, 这在创建对象树时非常方便。
生成器在执行制造步骤时, 不能对外发布未完成的产品。 这可以避免客户端代码获取到不完整结果对象的情况。
-
3. Java实现
-
UML类图
-
角色说明
- 产品:这里就是类图中的
CommonHouse
和SpecialHouse
,对应一个具体产品对象 - 客户端:就是客户端
Client
,调用指挥者Director
创建对象 - 抽象建造者:这个建造者就是创建对象的类,定义了创建产品的功能,它是一种规范,所以它可以是接口或者抽象类,对应类图中的
Builder
- 具体建造者:实际上创建产品的类,继承了抽象建造者,有多个不同的具体建造者,生产不同的对象,对应了图中的两个类
CommonHouseBuilder
和SpecialHouseBuilder
- 指挥者:对应图中的
Director
,顾名思义,用于指挥具体建造者建造对象,这个可能比较难理解,看代码!!!
- 产品:这里就是类图中的
-
Java代码
-
产品对象
/** * @Author: chy * @Description: 产品类:普通平房,没有特殊操作 * @Date: Create in 14:05 2021/3/5 */ public class CommonHouse { // 门,必选参数 private String door; // 窗,必选参数 private String window; // 游泳池,非必选参数 private String pool; public void setDoor(String door) { this.door = door; } public void setWindow(String window) { this.window = window; } public void setPool(String pool) { this.pool = pool; } @Override public String toString() { return "CommonHouse{" + "door='" + door + '\'' + ", window='" + window + '\'' + ", pool='" + pool + '\'' + '}'; } }
下面这个也是产品对象,但是它多了一个方法
fun()
。用以区分两个不同的产品(这里的区别可能有点小,大家意会即可)/** * @Author: chy * @Description: 产品类:特色房子,有特殊功能 * @Date: Create in 14:19 2021/3/5 */ public class SpecialHouse { // 门,必选参数 private String door; // 窗,必选参数 private String window; // 游泳池,非必选参数 private String pool; public void setDoor(String door) { this.door = door; } public void setWindow(String window) { this.window = window; } public void setPool(String pool) { this.pool = pool; } @Override public String toString() { return "CommonHouse{" + "door='" + door + '\'' + ", window='" + window + '\'' + ", pool='" + pool + '\'' + '}'; } void fun(){ System.out.println("特殊功能~~~"); } }
-
抽象建造者
/** * @Author: chy * @Description: 抽象建造者 * @Date: Create in 14:08 2021/3/5 */ public abstract class Builder { protected abstract Builder setDoor(String door); protected abstract Builder setWindow(String window); protected abstract Builder setPool(String pool); protected abstract Object getProduct(); }
-
具体建造者
不同的产品有不同的具体建造者,需要把它们内置到它们对应的具体建造者里面。
/** * @Author: chy * @Description: 具体建造者:普通平房建造者,生产普通平房 * @Date: Create in 14:09 2021/3/5 */ public class CommonHouseBuilder extends Builder{ // 内置产品对象 private CommonHouse commonHouse; public CommonHouseBuilder() { this.commonHouse = new CommonHouse(); } @Override protected Builder setDoor(String door) { commonHouse.setDoor(door); return this; } @Override protected Builder setWindow(String window) { commonHouse.setWindow(window); return this; } @Override protected Builder setPool(String pool) { commonHouse.setPool(pool); return this; } @Override protected CommonHouse getProduct() { return commonHouse; } }
/** * @Author: chy * @Description: 具体建造者:特殊房子建造者,生产特殊房子 * @Date: Create in 14:21 2021/3/5 */ public class SpecialHouseBuilder extends Builder{ // 内置产品对象 private SpecialHouse specialHouse; public SpecialHouseBuilder(){ specialHouse = new SpecialHouse(); } @Override protected Builder setDoor(String door) { specialHouse.setDoor(door); return this; } @Override protected Builder setWindow(String window) { specialHouse.setWindow(window); return this; } @Override protected Builder setPool(String pool) { specialHouse.setPool(pool); return this; } @Override protected SpecialHouse getProduct() { return specialHouse; } }
-
指挥者
指挥者内置了抽象建造者,我们构造指挥者的时候,只需要把具体建造者传给他就OK,指挥者里面也写了一系列建造的方法。
/** * @Author: chy * @Description: 指挥者 * @Date: Create in 14:13 2021/3/5 */ public class Director { private Builder builder; public Director(Builder builder) { this.builder = builder; } /** * 建一个4个窗2个门0游泳池的房子 */ void buildHouse_2D_4W_0P(){ builder.setDoor("两个门").setWindow("四个窗").setPool("没有游泳池"); } /** * 建一个2个窗2个门1游泳池的房子 */ void buildHouse_2D_2W_1P(){ builder.setDoor("两个门").setWindow("两个个窗").setPool("一个游泳池"); } }
-
客户端
/** * @Author: chy * @Description: 客户端 * @Date: Create in 16:30 2021/3/4 */ public class Client { public static void main(String[] args) { // 建平房 CommonHouseBuilder commonHouseBuilder = new CommonHouseBuilder(); Director director = new Director(commonHouseBuilder); director.buildHouse_2D_4W_0P(); CommonHouse commonHouse = commonHouseBuilder.getProduct(); System.out.println(commonHouse); System.out.println("===================="); // 建特殊房子 SpecialHouseBuilder specialHouseBuilder = new SpecialHouseBuilder(); Director director1 = new Director(specialHouseBuilder); director1.buildHouse_2D_2W_1P(); SpecialHouse specialHouse = specialHouseBuilder.getProduct(); specialHouse.fun(); System.out.println(specialHouse); } }
-
-
说明
如上面的代码,我们在客户端中只要新建一个
Director()
,然后传入一个具体建造者,调用指挥方法就可以获得我们所要的对象。用一个例子来比喻,产品类就相当于产品,具体建造者相当于工人,指挥者相当于包工头,客户端相当于甲方。所以逻辑就是:甲方提出需求(客户端调用
Director
)—> 包工头指挥具体工人工作(Director
指挥Builder
) --> 工人建房子(具体建造者构建对象)。回到最上面的建房子的问题,建造者能怎么解决?和其他两种方式又有什么不一样?
- 对比第一种,第一种我们可以很清楚的看出来类爆炸的问题,在建造者模式中,要扩展一个对象又该怎么做呢?其实只需要在指挥者中写一个指挥方法即可。当然,如果你要新建一种新的产品,还是需要写一个新的具体建造者的。
- 对比第二种,由于产品类具有可选参数的存在,所以构造函数难以书写,建造者模式也解决了这个问题,使用建造者模式创建实例化对象的时候,无需使用构造器,也就不存在这个问题,其实建造者模式构建对象更像是JavaBean的构建方法,即使用get和set来设置属性,但是它把这一步转移到了指挥者中,这样做即减少了耦合,也允许在设置属性的同时做出一些别的操作,不仅仅只是单纯的set和get(比如在指挥方法中我们可以随便做操作)
其实还存在最后一个问题,就是我们可能构建的对象的属性都是随时变化的,那使用建造者模式又必须让Director
去调用具体建造者,这样就每次都要在Director
新建一个新的方法,这样谁顶得住,那我们换个角度,能不能把客户端和指挥者两个角色融合,这样就可以直接在客户端中构建对象。
答案是可以,其实我上面的代码并非真正的建造者模式,而是对他做了一个修改,这样我们既可以使用Director
调用具体建造者构造对象,也可以在客户端中直接构建对象。
/**
* @Author: chy
* @Description: 客户端
* @Date: Create in 16:30 2021/3/4
*/
public class Client {
public static void main(String[] args) {
// 使用director
SpecialHouseBuilder specialHouseBuilder = new SpecialHouseBuilder();
Director director1 = new Director(specialHouseBuilder);
director1.buildHouse_2D_2W_1P();
SpecialHouse specialHouse = specialHouseBuilder.getProduct();
specialHouse.fun();
System.out.println(specialHouse);
System.out.println("====================");
// 客户端变成director
SpecialHouse product = (SpecialHouse)specialHouseBuilder.setDoor("三个门")
.setPool("一个游泳池")
.setWindow("不要窗")
.getProduct();
System.out.println(product);
}
}
理解:可以把Director
里的指挥方法看成是一个模板,如果要大量创建相同的对象,就可以直接使用Director
里的方法;如果是要构建大量不同的对象,就可以直接在客户端中构建对象。