建造(Builder)模式
建造模式可以将一个产品的内部表象与产品的生成过程分割开来,从而可以使一个建造过程生成具有不同的内部表象的产品对象。
摘自EffectiveJava:当构造方法参数过多时使用建造者模式。
产品的内部表象
一个产品常有不同的组成成分作为产品的零件,这些零件有可能是对象,也有可能不是对象,它们通常又叫做产品的内部表象(internal representation)。不同的产品可以有不同的内部表象,也就是不同的零件。使用建造模式可以使客户端不需要知道所生成的产品有哪些零件,每个产品的对应零件彼此有何不同,是怎么建造出来的,以及怎么组成产品。
对象性质的建造
有些情况下,一个对象会有一些重要的性质,在它们没有恰当的值之前,对象不能作为一个完整的产品使用。比如,一个电子邮件有发件人地址、收件人地址、主题、内容、附录等部分,而在最起码的收件人地址得到赋值之前,这个电子邮件不能发送。
有些情况下,一个对象的一些性质必须按照某个顺序赋值才有意义。在某个性质没有赋值之前,另一个性质则无法赋值。这些情况使得性质本身的建造涉及到复杂的商业逻辑。这时候,此对象相当于一个有待建造的产品,而对象的这些性质相当于产品的零件,建造产品的过程是建造零件的过程。由于建造零件的过程很复杂,因此,这些零件的建造过程往往被“外部化”到另一个称做建造者的对象里,建造者对象返还给客户端的是一个全部零件都建造完毕的产品对象。
建造模式利用一个导演者对象和具体建造者对象一个个地建造出所有的零件,从而建造出完整的产品对象。建造者模式将产品的结构和产品的零件的建造过程对客户端隐藏起来,把对建造过程进行指挥的责任和具体建造者零件的责任分割开来,达到责任划分和封装的目的
1. 简单的构造模式
UML类图:
代码如下:
package cn.qlq.builder; public class Product { /**
* 定义一些关于产品的操作
*/
private String part1;
private String part2; public String getPart1() {
return part1;
} public void setPart1(String part1) {
this.part1 = part1;
} public String getPart2() {
return part2;
} public void setPart2(String part2) {
this.part2 = part2;
}
}
package cn.qlq.builder; public interface Builder { public void buildPart1(); public void buildPart2(); public Product retrieveResult();
}
package cn.qlq.builder; public class ConcreteBuilder implements Builder { private Product product = new Product(); @Override
public void buildPart1() {
product.setPart1("part1");
} @Override
public void buildPart2() {
product.setPart2("part2");
} @Override
public Product retrieveResult() {
return product;
} }
package cn.qlq.builder; public class Director { private Builder builder; public Director(Builder builder) {
this.builder = builder;
} public void construct() {
builder.buildPart1();
builder.buildPart2();
} }
客户端代码:
package cn.qlq.builder; public class Client { public static void main(String[] args) {
Builder builder = new ConcreteBuilder();
Director director = new Director(builder);
director.construct();
Product product = builder.retrieveResult();
System.out.println(product.getPart1());
System.out.println(product.getPart2());
} }
上面的产品Product只有两个零件,即part1和part2。相应的建造方法也有两个:buildPart1()和buildPart2()、同时可以看出本模式涉及到四个角色,它们分别是:
抽象建造者(Builder)角色:给 出一个抽象接口,以规范产品对象的各个组成成分的建造。一般而言,此接口独立于应用程序的商业逻辑。模式中直接创建产品对象的是具体建造者 (ConcreteBuilder)角色。具体建造者类必须实现这个接口所要求的两种方法:一种是建造方法(buildPart1和 buildPart2),另一种是返还结构方法(retrieveResult)。一般来说,产品所包含的零件数目与建造方法的数目相符。换言之,有多少 零件,就有多少相应的建造方法。
具体建造者(ConcreteBuilder)角色:担任这个角色的是与应用程序紧密相关的一些类,它们在应用程序调用下创建产品的实例。这个角色要完成的任务包括:1.实现抽象建造者Builder所声明的接口,给出一步一步地完成创建产品实例的操作。2.在建造过程完成后,提供产品的实例。
导演者(Director)角色:担任这个角色的类调用具体建造者角色以创建产品对象。应当指出的是,导演者角色并没有产品类的具体知识,真正拥有产品类的具体知识的是具体建造者角色。
产品(Product)角色:产品便是建造中的复杂对象。一般来说,一个系统中会有多于一个的产品类,而且这些产品类并不一定有共同的接口,而完全可以是不相关联的
省略抽象建造者角色:
如果只有一个具体建造者的话可以省略掉抽象建造者角色。
省略导演组角色:
在具体建造者只有一个的情况下,如果抽象者角色已经被省略掉,那么导演者角色也可以省略掉。
Builder就自己扮演了导演者和建造者的双角色。此时需要改变Builder类,如下:
package cn.qlq.builder; public class ConcreteBuilder { private Product product = new Product(); public void buildPart1() {
product.setPart1("part1");
} public void buildPart2() {
product.setPart2("part2");
} public Product retrieveResult() {
return product;
} /**
* 生产最终的产品
*
* @return
*/
public Product construct() {
buildPart1();
buildPart2();
return product;
} }
试用场景:
(1)需要生成的产品具有复杂的内部结构,也就是成员属性比较多
(2)需要生成的产品的属性相互依赖时。建造者模式可以强制实行一种分步骤进行的建造过程。
(3)在对象的创建过程中会使用系统的一些其他对象,这些对象在产品的创建过程中不易得到
优点:
(1)建造者模式使得产品的内部表象可以独立地变化。使用建造者模式可以使客户端不必知道产品内部组成的细节
(2)每一个builder都相互独立而与其他无关
(3)模式所建造的最终产品更易于控制。
建造者模式与抽象工程模式区别:
在抽象工程模式中,每次调用工程都会返回一个完整的产品对象,客户端决定使用这些产品组成一个或者多个更复杂产品对象。
建造者模式则不同,它一点一点地创建出一个复杂的产品,而这个产品的组装就发生在建造者角色内部。建造者模式的客户端拿到的是一个完整的最后产品。
抽象工厂模式和工厂模式都是设计模式,但是抽象工厂处在更加具体的尺度上,而建造者模式处在更加宏观的尺度上。一个系统可以由一个抽象工厂 + 一个建造模式组成,客户端通过调用这个建造角色,间接地调用另一个抽象工厂的工厂角色。工厂模式反还不同的产品,建造角色把他们组装起来。
2. 建造者模式的应用
假设有一个电子杂志系统,定期地向用户的电子邮件信箱发送电子杂志。用户可以通过网页订阅电子杂志,也可以通过网页结束订阅。当客户开始订阅时,系统发送一个电子邮件表示欢迎,当客户结束订阅时,系统发送一个电子邮件表示欢送。本例子就是这个系统负责发送“欢迎”和“欢送”邮件的模块。
在本例中,产品类就是发给某个客户的“欢迎”和“欢送”邮件。
UML类图如下:
代码如下:
package cn.qlq.builder; import java.util.Date; public abstract class AutoMessage { protected String subject = "";
protected String body = "";
protected String from = "";
protected String to = "";
protected Date sendDate = null; public AutoMessage() {
super();
} public void send() {
System.out.println("subject - > " + subject);
System.out.println("body - > " + body);
System.out.println("from - > " + from);
System.out.println("to - > " + to); System.out.println("发送邮件");
} public String getSubject() {
return subject;
} public void setSubject(String subject) {
this.subject = subject;
} public String getBody() {
return body;
} public void setBody(String body) {
this.body = body;
} public String getFrom() {
return from;
} public void setFrom(String from) {
this.from = from;
} public String getTo() {
return to;
} public void setTo(String to) {
this.to = to;
} public Date getSendDate() {
return sendDate;
} public void setSendDate(Date sendDate) {
this.sendDate = sendDate;
} }
package cn.qlq.builder; public class WelcomeMessage extends AutoMessage { public WelcomeMessage() {
System.out.println("WelcomeMessage");
} public void sayWelcome() {
System.out.println("welcome ...");
} }
package cn.qlq.builder; public class GoodbyeMsgBuilder extends Builder { public GoodbyeMsgBuilder() {
autoMessage = new WelcomeMessage();
} @Override
void buildSubject() {
autoMessage.setSubject("欢送邮件主体");
} @Override
void buildBody() {
autoMessage.setBody("欢送邮件内容");
} }
package cn.qlq.builder; import java.util.Date; public abstract class Builder { protected AutoMessage autoMessage; abstract void buildSubject(); abstract void buildBody(); void buildFrom(String from) {
autoMessage.setFrom(from);
} void buildTo(String to) {
autoMessage.setTo(to);
} void buildSendDate() {
autoMessage.setSendDate(new Date());
} void sendMessage() {
autoMessage.send();
}
}
package cn.qlq.builder; public class WelcomeMsgBuilder extends Builder { public WelcomeMsgBuilder() {
autoMessage = new WelcomeMessage();
} @Override
void buildSubject() {
autoMessage.setSubject("欢迎邮件主体");
} @Override
void buildBody() {
autoMessage.setBody("欢迎邮件内容");
} }
package cn.qlq.builder; public class GoodbyeMsgBuilder extends Builder { public GoodbyeMsgBuilder() {
autoMessage = new WelcomeMessage();
} @Override
void buildSubject() {
autoMessage.setSubject("欢送邮件主体");
} @Override
void buildBody() {
autoMessage.setBody("欢送邮件内容");
} }
package cn.qlq.builder; public class Director { Builder builder; public Director(Builder builder) {
this.builder = builder;
} public void construct(String from, String to) {
builder.buildFrom(from);
builder.buildTo(to);
builder.buildSendDate();
builder.buildSubject();
builder.buildBody(); builder.sendMessage();
} public Builder getBuilder() {
return builder;
} public void setBuilder(Builder builder) {
this.builder = builder;
} }
测试代码:
package cn.qlq.builder; public class MainClass { public static void main(String[] args) {
Builder builder = new WelcomeMsgBuilder();
Director director = new Director(builder); director.construct("78987857@qq.com", "954185@qq.com");
} }
结果:
WelcomeMessage
subject - > 欢迎邮件主体
body - > 欢迎邮件内容
from - > 78987857@qq.com
to - > 954185@qq.com
发送邮件
3. 当构造方法参数过多时使用建造者模式
比如一个User,有好多属性,但是只有ID是必须有的,其他属性可有可无。如下:
package cn.qlq.builder; public class User { // 必须字段
private int id; private String name;
private String sex;
private String job;
private String health;
private String BMI;
private int height;
private int weight; public User() {
super();
} public User(int id, String name, String sex, String job, String health, String bMI, int height, int weight) {
super();
this.id = id;
this.name = name;
this.sex = sex;
this.job = job;
this.health = health;
BMI = bMI;
this.height = height;
this.weight = weight;
} public int getId() {
return id;
} public void setId(int id) {
this.id = id;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public String getSex() {
return sex;
} public void setSex(String sex) {
this.sex = sex;
} public String getJob() {
return job;
} public void setJob(String job) {
this.job = job;
} public String getHealth() {
return health;
} public void setHealth(String health) {
this.health = health;
} public String getBMI() {
return BMI;
} public void setBMI(String bMI) {
BMI = bMI;
} public int getHeight() {
return height;
} public void setHeight(int height) {
this.height = height;
} public int getWeight() {
return weight;
} public void setWeight(int weight) {
this.weight = weight;
} @Override
public String toString() {
return "User [id=" + id + ", name=" + name + ", sex=" + sex + ", job=" + job + ", health=" + health + ", BMI="
+ BMI + ", height=" + height + ", weight=" + weight + "]";
} }
当我们设置几个属性的时候可以通过构造方法进行创建,但是比如我们只想设置一些属性,其他属性没用,我们可能会写成空值,这样的代码阅读起来很难懂,属性更多的时候更加难以阅读:
User user = new User(1, "张三", "", "", "", "", 0, 0);
也有可能通过setter进行设值,如下:(属性更多的时候需要更多的setter)
User user = new User();
user.setId(1);
user.setName("xxx");
user.setBMI("XXX");
...
解决办法:采用建造模式 + 流式写法
由于是用Builder模式来创建某个对象,因此就没有必要再定义一个Builder接口,直接提供一个具体的建造者类就可以了。
对于创建一个复杂的对象,可能会有很多种不同的选择和步骤,干脆去掉“导演者”,把导演者的功能和Client的功能合并起来,也就是说,Client这个时候就相当于导演者,它来指导构建器类去构建需要的复杂对象。
package cn.qlq.builder; public class UserBuilder { private User user = new User(); /**
* 构造方法确保ID必有
*
* @param id
*/
public UserBuilder(int id) {
user.setId(id);
} UserBuilder name(String name) {
user.setName(name);
return this;
} UserBuilder sex(String sex) {
user.setSex(sex);
return this;
} UserBuilder job(String job) {
user.setJob(job);
return this;
} UserBuilder health(String health) {
user.setHealth(health);
return this;
} UserBuilder BMI(String BMI) {
user.setBMI(BMI);
return this;
} UserBuilder height(int height) {
user.setHeight(height);
return this;
} UserBuilder weight(int weight) {
user.setWeight(weight);
return this;
} public User build() {
if (user.getId() == 0) {
throw new RuntimeException("id必须设置");
} return user;
} }
客户端代码:
package cn.qlq.builder; public class MainClass { public static void main(String[] args) {
UserBuilder userBuilder = new UserBuilder(2);
User user = userBuilder.name("张三").BMI("xxx").health("健康").build();
System.out.println(user);
} }
这样的代码读起来也舒服,语义也更好理解。
总结:
意图:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
主要解决:主要解决在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
何时使用:一些基本部件不会变,而其组合经常变化的时候。
如何解决:将变与不变分离开。
关键代码:建造者:创建和提供实例,导演:管理建造出来的实例的依赖关系。
应用实例: 1、去肯德基,汉堡、可乐、薯条、炸鸡翅等是不变的,而其组合是经常变化的,生成出所谓的"套餐"。 2、JAVA 中的 StringBuilder。
优点: 1、建造者独立,易扩展。 2、便于控制细节风险。
缺点: 1、产品必须有共同点,范围有限制。 2、如内部变化复杂,会有很多的建造类。
使用场景: 1、需要生成的对象具有复杂的内部结构。 2、需要生成的对象内部属性本身相互依赖。
注意事项:与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。
建造(Builder)模式的更多相关文章
- 浅谈设计模式--建造器模式(Builder Pattern)
建造器模式,是于创建带有大量参数的对象,并避免因参数数量多而产生的一些问题(如状态不一致-JavaBean的setter模式). 如果参数多且有些是必须初始化的,有些是不一定需要初始化的时候,创建对象 ...
- 创建型模式之Builder模式及实现
建造者(Builder)模式 GOF给出的定义为:建造者模式是将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示. 应用场景 使用建造者模式是为了将构建复杂对象的过程和它的部件 ...
- Builder模式
原文来源于http://www.iteye.com/topic/71175 对于Builder模式很简单,但是一直想不明白为什么要这么设计,为什么要向builder要Product而不是向知道建造过程 ...
- [转]C++设计模式:Builder模式
Builder模式要解决的问题是,当我们要创建很复杂的对象时,有时候需要将复杂对象的创建过程和这个对象的表示分离开来.由于在每一步的构造过程中可以映入不同参数,所以步骤相同但是最后的对象却不一样.也就 ...
- 6. 星际争霸之php设计模式--建造器模式
题记==============================================================================本php设计模式专辑来源于博客(jymo ...
- Java Builder模式 体验(二)
在上篇文章中,对Java Builder模式的使用体验主要是从Builder对构造器改造方面的优秀特性来说的,感觉并没有从Java Builder模式本身的功能和作用去写,因此决定再从Build ...
- 建造者(Builder)模式
建造者模式是对象的创建模式.建造模式可以将一个产品的内部表象(internal representation)与产品的生产过程分割开来,从而可以使一个建造过程生成具有不同的内部表象的产品对象. 产品的 ...
- Java设计模式-建造者(Builder)模式
目录 由来 使用 1. 定义抽象 Builder 2. 定义具体 Builder类 3. 定义具体 Director类 4. 测试 定义 文字定义 结构图 优点 举例 @ 最近在看Mybatis的源码 ...
- 设计模式初学者笔记:Builder模式
[作者:byeyear Email:byeyear@hotmail.com 首发:cnblogs 转载请注明] 在本文的开头,先森森的鄙视下自己……将Builder模式反反复复读了七 ...
随机推荐
- 高性能TcpServer(Java) - Netty
源码下载 -> 提取码 QQ:505645074 Netty 是一个高性能.异步事件驱动的 NIO 框架,它提供了对 TCP.UDP 和文件传输的支持,作为一个异步 NIO 框架,Netty ...
- MySql定时备份脚本
最近需要对某服务的数据库数据进行备份,因此参考网上教程完成数据库备份脚本. 因为服务的使用频率较低,因此设置定时任务,在每天的中午以及午夜时分进行备份操作. #!/bin/bash # 设置mysql ...
- java获取客户端请求IP地址(公网ip)
之前写了一个获取ip地址的方法,但是放网上一查显示此Ip地址是局域网ip地址,要是想获取请求端的真实公网ip地址怎么样了,看了一些别人的博客后发现,想要获取客户端的公网ip必须借助第三方. packa ...
- MSSQL记录表字段数据变化的相关SQl
在软件实施过程中,也许会有这样的问题: 表中数据出现非预期的结果,此时不确定是程序问题,哪个程序,存储过程,触发器.. 或还是人为修改的结果,此时可以用触发器对特定的表字段做跟踪监视,记录每次新增,修 ...
- VirtualBox打开VMware虚拟机
下载安装VirtualBox 打开VirtualBox,选择新建 设置如下: 之后就可以直接打开虚拟机了.
- nginx 安装第三方模块(lua)并热升级
需求: nginx上将特定请求拒绝,并返回特定值. 解决办法: 使用lua脚本,实现效果. 操作步骤: 安装Luajit环境 重新编译nginx(目标机器上nginx -V 配置一致,并新增两个模块n ...
- python安装第三方库--换镜像源
python安装第三方库--换镜像源 1. 更换anaconda源 清华大学镜像:清华大学镜像 anaconda下载地址:https://mirrors.tuna.tsinghua.edu.cn/an ...
- 我以为我对Kafka很了解,直到我看了此文章
Kafka 是一个消息系统,原本开发自 LinkedIn,用作 LinkedIn 的活动流(Activity Stream)和运营数据处理管道(Pipeline)的基础. 现在它已被多家不同类型的公司 ...
- 调试CEF3程序的方法
CEF3多进程模式调试时按F5只会启动调试Browser进程,要调试Renderer进程就要让进程在启动时就暂停并附加进程. 所幸google早就想到了这一点,chrome的命令行参数就可以办到, - ...
- maven jar包冲突的发现与解决[工具篇]
本文是我的第177篇文章. 关于jar冲突排查解决的问题,相信很多小伙伴也都知道有一些,无非就是两类:命令 or 工具. 命令方式比如: mvn dependency:tree 工具方式比如: Mav ...