建造模式可以将一个产品的内部表象与产品的生成过程分割开来,从而可以使一个建造过程生成具有不同的内部表象的产品对象。
摘自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("[email protected]", "[email protected]"); } }
结果:
WelcomeMessage
subject - > 欢迎邮件主体
body - > 欢迎邮件内容
from - > [email protected]
to - > [email protected]
发送邮件
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); } }
这样的代码读起来也舒服,语义也更好理解。