概述
在软件开发过程中有时需要创建一个复杂的对象,这个复杂对象通常由多个子部件按一定的步骤组合而成。例如,计算机是由 CPU、主板、内存、硬盘、显卡、机箱、显示器、键盘、鼠标等部件组装而成的,采购员不可能自己去组装计算机,而是将计算机的配置要求告诉计算机销售公司,计算机销售公司安排技术人员去组装计算机,然后再交给要买计算机的采购员。
生活中这样的例子很多,如游戏中的不同角色,其性别、个性、能力、脸型、体型、服装、发型等特性都有所差异;还有汽车中的方向盘、发动机、车架、轮胎等部件也多种多样;每封电子邮件的发件人、收件人、主题、内容、附件等内容也各不相同。
以上所有这些产品都是由多个部件构成的,各个部件可以灵活选择,但其创建步骤都大同小异。这类产品的创建无法用前面介绍的工厂模式描述,只有建造者模式可以很好地描述该类产品的创建。
建造者(Builder)模式的定义:指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。(例如:下面的单车实例,一辆单车是一个复杂的对象,它由不同的机械器件构成,包括车架、车座等,这些机械器件就是一个个的简单对象,将其组装成一个完整的单车,就是建造,注意单车都是由这些部件组成的,但是我们可以自己决定车架用铝合金车架还是钛合金车架、车座用橡胶还是真皮。就像我们自己组装台式机电脑,虽然电脑还是那个电脑,但我们用不同厂商生产的电脑组件来搭配,而不是笔记本电脑的一体。)
结构
建造者(Builder)模式包含如下角色:
-
抽象建造者类(Builder):这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的部件对象的创建。(定义了建造产品子部件的抽象方法,如单车实例中就定义了建造组成单车的车座和车架的抽象方法。)
-
具体建造者类(ConcreteBuilder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供产品的实例。(具体的建造者,就是自定义各个部件用什么了,如在A具体建造者类中车座用真皮的,B具体建造者类中车座用橡胶的,这就是自定义的部分。)
-
产品类(Product):要创建的复杂对象。(即一辆单车、一台完整的电脑,上面的车座和车架只是产品的一些组成部分。而该类中的属性规定了组成复杂对象需要用到的简单对象,如单车的车座和车架等)
-
指挥者类(Director):调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。(将建造者类中的简单对象按某种顺序组装成一个完整的产品,也可以说指挥者类就是生产一个完整的产品,而不在乎内部的构造过程,也不在乎使用了哪些组件,用的谁的组件。)
UML类图如下:
各实现类如下:(第一步,先创建产品类,在产品类中决定了该产品由哪些简单的对象组成;第二步,创建抽象建造者,在抽象建造者中定义创建产品子部件的方法;第三步,创建具体建造者,在具体建造者对子部件进行自定义定制;第四步,创建指挥者,在指挥者中按照某种顺序完成产品的构建;第五步,客户端测试,先创建建造者,再创建指挥者,将建造者对象填入到指挥者中,然后调用指挥者的方法创建产品,最后就是对产品进行使用了。)
- Product.java
/**
* @author lcl100
* @create 2021-07-24 11:44
* @desc 产品角色,是一个复杂对象,里面包括组成该复杂对象的简单对象
*/
public class Product {
// 组成产品的A部件
private String partA;
// 组件产品的B部件
private String partB;
// 组成产品的C部件
private String partC;
public void setPartA(String partA) {
this.partA = partA;
}
public void setPartB(String partB) {
this.partB = partB;
}
public void setPartC(String partC) {
this.partC = partC;
}
public void show() {
// 产品的行为方法,跟建造者、指挥者无关
// 例如,展示该产品的特性
}
}
- Builder.java
/**
* @author lcl100
* @create 2021-07-24 11:47
* @desc 抽象建造者,包括创建产品各个子部件的抽象方法
*/
public abstract class Builder {
// 创建产品对象
protected Product product = new Product();
/**
* 建造产品的A部件
*/
public abstract void buildPartA();
/**
* 建造产品的B部件
*/
public abstract void buildPartB();
/**
* 建造产品的C部件
*/
public abstract void buildPartC();
/**
* 返回产品对象
*
* @return 产品对象
*/
public Product getResult() {
return product;
}
}
- ConcreteBuilder.java
/**
* @author lcl100
* @create 2021-07-24 11:51
* @desc 具体建造者,实现了抽象建造者接口
*/
public class ConcreteBuilder extends Builder {
@Override
public void buildPartA() {
// 通过产品的set()方法来将建造完成的子部件设置到产品中
product.setPartA("建造PartA");
}
@Override
public void buildPartB() {
product.setPartB("建造PartB");
}
@Override
public void buildPartC() {
product.setPartC("建造PartC");
}
}
- Director.java
/**
* @author lcl100
* @create 2021-07-24 11:54
* @desc 指挥者,调用建造者中的方法完成复杂对象的创建,只与建造者Builder打交道
*/
public class Director {
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
/**
* 指挥者指挥产品的构建与组装
*
* @return 返回组建成功的产品
*/
public Product construct() {
// 都是调用建造者Builder的方法完成
builder.buildPartA();
builder.buildPartB();
builder.buildPartC();
return builder.getResult();
}
}
- Client.java
/**
* @author lcl100
* @create 2021-07-24 11:58
* @desc 测试类
*/
public class Client {
public static void main(String[] args) {
// 创建建造者
Builder builder = new ConcreteBuilder();
// 创建指挥者
Director director = new Director(builder);
// 通过指挥者来创建产品
Product product = director.construct();
// 查看产品的特性,即调用产品独有的方法
product.show();
}
}
实例
生产自行车是一个复杂的过程,它包含了车架,车座等组件的生产。而车架又有碳纤维,铝合金等材质的,车座有橡胶,真皮等材质。对于自行车的生产就可以使用建造者模式。
这里Bike是产品,包含车架,车座等组件;Builder是抽象建造者,MobikeBuilder和OfoBuilder是具体的建造者;Director是指挥者。UML类图如下:
代码如下:
- Bike.java
public class Bike {
private String frame;// 车架
private String seat;// 坐架
public String getFrame() {
return frame;
}
public void setFrame(String frame) {
this.frame = frame;
}
public String getSeat() {
return seat;
}
public void setSeat(String seat) {
this.seat = seat;
}
}
- Builder.java
public abstract class Builder {
//声明Bike类型的变量,并进行赋值
protected Bike bike = new Bike();
/**
* 构建车架
*/
public abstract void buildFrame();
/**
* 构建坐架
*/
public abstract void buildSeat();
/**
* 创建单车
* @return
*/
public abstract Bike createBike();
}
- MobileBuilder.java
public class MobileBuilder extends Builder {
@Override
public void buildFrame() {
bike.setFrame("碳纤维车架");
}
@Override
public void buildSeat() {
bike.setSeat("真皮车座");
}
@Override
public Bike createBike() {
return bike;
}
}
- OfoBuilder.java
public class OfoBuilder extends Builder {
@Override
public void buildFrame() {
bike.setFrame("铝合金车架");
}
@Override
public void buildSeat() {
bike.setSeat("橡胶车座");
}
@Override
public Bike createBike() {
return bike;
}
}
- Director.java
public class Director {
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
/**
* 组装单车的功能
*
* @return 返回组装成功的单车对象
*/
public Bike construct() {
builder.buildFrame();
builder.buildSeat();
return builder.createBike();
}
}
- Test.java
public class Test {
public static void main(String[] args) {
// 创建指挥者对象
Director director=new Director(new MobileBuilder());
// 让指挥者只会组装自行车
Bike bike = director.construct();
// 打印结果
System.out.println(bike.getFrame());
System.out.println(bike.getSeat());
}
}
上面示例是 Builder模式的常规用法,指挥者类 Director 在建造者模式中具有很重要的作用,它用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但是有些情况下需要简化系统结构,可以把指挥者类和抽象建造者进行结合。
// 抽象 builder 类
public abstract class Builder {
protected Bike bike= new Bike();
public abstract void buildFrame();
public abstract void buildSeat();
public abstract Bike createBike();
public Bike construct() {
this.buildFrame();
this.BuildSeat();
return this.createBike();
}
}
注意:这样做确实简化了系统结构,但同时也加重了抽象建造者类的职责,也不是太符合单一职责原则,如果construct() 过于复杂,建议还是封装到 Director 中。
优缺点
该模式的主要优点如下:
-
建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在指挥者类中对整体而言可以取得比较好的稳定性。
-
在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
-
可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
-
建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。符合开闭原则。
其缺点如下:
- 产品的组成部分必须相同,这限制了其使用范围。
- 如果产品的内部变化复杂,如果产品内部发生变化,则建造者也要同步修改,后期维护成本较大。
建造者模式主要适用于以下应用场景:
- 相同的方法,不同的执行顺序,产生不同的结果。
- 多个部件或零件,都可以装配到一个对象中,但是产生的结果又不相同。
- 产品类非常复杂,或者产品类中不同的调用顺序产生不同的作用。
- 初始化一个对象特别复杂,参数多,而且很多参数都具有默认值。
模式扩展
建造者模式除了上面的用途外,在开发中还有一个常用的使用方式,就是当一个类构造器需要传入很多参数时,如果创建这个类的实例,代码可读性会非常差,而且很容易引入错误,此时就可以利用建造者模式进行重构。(即创建一个Builder内部类)
重构前代码如下:
public class Phone {
private String cpu;
private String screen;
private String memory;
private String mainboard;
public Phone(String cpu, String screen, String memory, String mainboard) {
this.cpu = cpu;
this.screen = screen;
this.memory = memory;
this.mainboard = mainboard;
}
public String getCpu() {
return cpu;
}
public void setCpu(String cpu) {
this.cpu = cpu;
}
public String getScreen() {
return screen;
}
public void setScreen(String screen) {
this.screen = screen;
}
public String getMemory() {
return memory;
}
public void setMemory(String memory) {
this.memory = memory;
}
public String getMainboard() {
return mainboard;
}
public void setMainboard(String mainboard) {
this.mainboard = mainboard;
}
@Override
public String toString() {
return "Phone{" +
"cpu='" + cpu + '\'' +
", screen='" + screen + '\'' +
", memory='" + memory + '\'' +
", mainboard='" + mainboard + '\'' +
'}';
}
}
public class Client {
public static void main(String[] args) {
//构建Phone对象
Phone phone = new Phone("intel","三星屏幕","金士顿","华硕");
System.out.println(phone);
}
}
上面在客户端代码中构建Phone对象,传递了四个参数,如果参数更多呢?代码的可读性及使用的成本就是比较高。
重构后代码:
public class Phone {
private String cpu;
private String screen;
private String memory;
private String mainboard;
private Phone(Builder builder) {
cpu = builder.cpu;
screen = builder.screen;
memory = builder.memory;
mainboard = builder.mainboard;
}
public static final class Builder {
private String cpu;
private String screen;
private String memory;
private String mainboard;
public Builder() {}
public Builder cpu(String val) {
cpu = val;
return this;
}
public Builder screen(String val) {
screen = val;
return this;
}
public Builder memory(String val) {
memory = val;
return this;
}
public Builder mainboard(String val) {
mainboard = val;
return this;
}
public Phone build() {
return new Phone(this);}
}
@Override
public String toString() {
return "Phone{" +
"cpu='" + cpu + '\'' +
", screen='" + screen + '\'' +
", memory='" + memory + '\'' +
", mainboard='" + mainboard + '\'' +
'}';
}
}
public class Client {
public static void main(String[] args) {
Phone phone = new Phone.Builder()
.cpu("intel")
.mainboard("华硕")
.memory("金士顿")
.screen("三星")
.build();
System.out.println(phone);
}
}
重构后的代码在使用起来更方便,某种程度上也可以提高开发效率。从软件设计上,对程序员的要求比较高。
再写一个用得到的实例,如果写爬虫写得多的话,经常需要创建请求头,通常我们是使用HashMap来处理,下面看看用建造者模式来解决。
- RequestHeader.java
import java.util.HashMap;
import java.util.Map;
/**
* @author lcl100
* @create 2021-07-24 12:14
* @desc 请求头的设置,采用建造者模式
*/
public class RequestHeader {
private String host;
private String userAgent;
private String accept;
private String acceptLanguage;
private String acceptEncoding;
private String referer;
private String connection;
private String cookie;
private String origin;
private RequestHeader(Builder builder) {
this.host = builder.host;
this.userAgent = builder.userAgent;
this.accept = builder.accept;
this.acceptLanguage = builder.acceptLanguage;
this.acceptEncoding = builder.acceptEncoding;
this.referer = builder.referer;
this.connection = builder.connection;
this.cookie = builder.cookie;
this.origin = builder.origin;
}
public Map<String, String> getHeaders() {
Map<String, String> headers = new HashMap<>();
if (host != null && !"".equals(host)) {
headers.put("Host", host);
}
if (userAgent != null && !"".equals(userAgent)) {
headers.put("User-Agent", userAgent);
}
if (accept != null && !"".equals(accept)) {
headers.put("Accept", accept);
}
if (acceptLanguage != null && !"".equals(acceptLanguage)) {
headers.put("Accept-Language", acceptLanguage);
}
if (acceptEncoding != null && !"".equals(acceptEncoding)) {
headers.put("Accept-Encoding", acceptEncoding);
}
if (referer != null && !"".equals(referer)) {
headers.put("Referer", referer);
}
if (connection != null && !"".equals(connection)) {
headers.put("Connection", connection);
}
if (cookie != null && !"".equals(cookie)) {
headers.put("Cookie", cookie);
}
if (origin != null && !"".equals(origin)) {
headers.put("Origin", origin);
}
return headers;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
Map<String, String> headers = getHeaders();
for (String key : headers.keySet()) {
sb.append(key + ":" + headers.get(key) + "\n");
}
return sb.toString();
}
public static final class Builder {
private String host;
private String userAgent;
private String accept;
private String acceptLanguage;
private String acceptEncoding;
private String referer;
private String connection;
private String cookie;
private String origin;
public Builder() {
}
public Builder host(String val) {
this.host = val;
return this;
}
public Builder userAgent(String val) {
this.userAgent = val;
return this;
}
public Builder accept(String val) {
this.accept = val;
return this;
}
public Builder acceptLanguage(String val) {
this.acceptLanguage = val;
return this;
}
public Builder acceptEncoding(String val) {
this.acceptEncoding = val;
return this;
}
public Builder referer(String val) {
this.referer = val;
return this;
}
public Builder connection(String val) {
this.connection = val;
return this;
}
public Builder cookie(String val) {
this.cookie = val;
return this;
}
public Builder origin(String val) {
this.origin = val;
return this;
}
public RequestHeader build() {
return new RequestHeader(this);
}
}
}
- Client.java
/**
* @author lcl100
* @create 2021-07-24 12:15
*/
public class Client {
public static void main(String[] args) {
before();
after();
}
/**
* 采用建造者模式之前创建请求头的方法
*/
public static void before() {
// 创建请求头
Map<String, String> headers = new HashMap<>();
headers.put("Accept", "image/webp,*/*");
headers.put("Accept-Encoding", "gzip, deflate, br");
headers.put("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2");
headers.put("Connection", "keep-alive");
headers.put("Cookie", "__yjs_duid=1_5ac5370244e75e94b6b4846a999e60ad1619953026830;");
headers.put("Host", "www.baidu.com");
headers.put("Referer", "https://www.baidu.com/");
headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0");
// 使用请求头
}
/**
* 采用建造者模式之后创建请求头的方法
*/
public static void after() {
// 创建请求头
RequestHeader requestHeader = new RequestHeader.Builder()
.accept("image/webp,*/*")
.acceptEncoding("gzip, deflate, br")
.acceptLanguage("zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2")
.connection("keep-alive")
.cookie("__yjs_duid=1_5ac5370244e75e94b6b4846a999e60ad1619953026830;")
.host("www.baidu.com")
.referer("https://www.baidu.com/")
.userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0")
.build();
Map<String, String> headers = requestHeader.getHeaders();
// 使用请求头
System.out.println(requestHeader.toString());
}
}
创建者模式对比
工厂方法模式与建造者模式
工厂方法模式注重的是整体对象的创建方式;而建造者模式注重的是部件构建的过程,意在通过一步一步地精确构造创建出一个复杂的对象。
我们举个简单例子来说明两者的差异,如要制造一个超人,如果使用工厂方法模式,直接产生出来的就是一个力大无穷、能够飞翔、内裤外穿的超人;而如果使用建造者模式,则需要组装手、头、脚、躯干等部分,然后再把内裤外穿,于是一个超人就诞生了。
抽象工厂模式与建造者模式
抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式则是不需要关心构建过程,只关心什么产品由什么工厂生产即可。
建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品。
如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车。
建造者模式与工厂模式
- 建造者模式更加注重方法的调用顺序,工厂模式注重创建对象。
- 创建对象的力度不同,建造者模式创建复杂的对象,由各种复杂的部件组成,工厂模式创建出来的对象都一样
- 关注重点不一样,工厂模式只需要把对象创建出来就可以了,而建造者模式不仅要创建出对象,还要知道对象由哪些部件组成。
- 建造者模式根据建造过程中的顺序不一样,最终对象部件组成也不一样。