理解建造者模式

建造者模式我们都接触过,在开发中,经常见到 XXXBuilder 这样的类,通常以这种方式命名的类就使用了建造者模式。

在《设计模式-可复用面向对象软件基础》一书中,建造者模式的定义是:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示

直接看这个定义可能相当抽象,不知所云。还是通过一个例子对定义进行说明。

例子引入

在如今这个可以放肆偷懒的社会中,我们每天必做的事情一定有叫外卖。在 APP 上,随便进入一家店浏览,必定有套餐推荐。我们就以这个套餐为例,对建造者模式进行说明。现有这样的一家店铺,该店铺套餐有很多种,其中包含有‘烈焰’套餐和‘冰爽’套餐。这两种套餐都包含了主食、配菜、小吃、饮料。两种套餐的餐品表如下:

  1. ‘烈焰’套餐:辣子鸡套饭(主食)+ 酱菜(配菜)+ 泡椒凤爪(小吃)+ 冰红茶(饮料)
  2. ‘冰爽’套餐:薄荷焖饭(主食)+ 泡菜(配菜)+ 凉拌海带丝(小吃)+ 雪碧(饮料)

该如何描述客人点餐及准备套餐这样的过程?

问题分析

  • 消费者:作为消费者,不需要在 APP 中逐个选择主食、配菜、小吃、饮料这令人头大的事情,只需要选择‘烈焰’套餐还是‘冰爽’套餐即可;
  • 商家老板:作为老板,只需要根据 APP 中顾客所点的套餐类型分别安排手下的员工准备对应的套餐即可;
  • 商家员工工作手册:为统一店铺管理,指定了员工工作手册,该手册制定了员工应如何准备各个餐品,如何包装套餐等;
  • 商家员工:按照手册的规定及老板的要求,准备具体的套餐,各套餐中分别放入主食、配菜、小吃、饮料;
  • 套餐:套餐准备完毕后,装入一个外卖盒中,安排骑手送到消费者手中即可。

在上述的工作流程分析中,已经完整包含了建造者模式的四个要素。这四个要素分别是:

  • Builder:为创建一个产品对象的各个部件指定抽象接口,等效于员工工作手册,该组件用于规范工作流程,约束每个员工在每一个套餐的制作中至少应该完成一些什么样的工作;
  • ConcreteBuilder: 实现 Builder 的接口以构造和装配该产品的各个部件 ,等效于商家员工,负责构造和装配一个完整的产品;
  • Director: 构造一个使用 Builder 接口的对象 ,产品需要具体的 Builder 来生产,而 Director 是产生具体的 Builder 的。把 Director 比作商家老板,老板不负责制作套餐,他只负责根据顾客的需求安排具体的商家员工来制作套餐;
  • Product:表示被构造的复杂对象。相当于这里已经打包好的具体的套餐外卖。

按照这样的工作流程分析:

  • 对消费者更加友好:消费者不必再分别点餐主食、配菜、小吃、饮料,甚至消费者并不需要关心套餐里面都有些什么,只需要记得一个好吃的套餐名字即可,对于有选择恐惧症的顾客来说,这无疑是相当友好的;
  • 店铺的运转更好:专人干专事,老板负责协调指挥下属员工而不用去生成产品(不用前台和后厨来回跑,因为老板只与下属员工交接,不关心后厨的工作内容);员工也不需要面对顾客的需求,只需要听老板的指挥,然后生产对应的套餐即可;
  • 店铺的业务拓展更方便:今后店铺如需再增加一个套餐,只需要增加一种配制该类型套餐的员工即可。

    类图分析

    按照上诉分析中的四个要素,接着看一下建造者模式的定义。
    创建型 - 建造者模式(Builder) - 图1

    解析模式定义

    有了这个例子的基础,再尝试理解建造者模式的定义就会轻松很多。

  • 将一个复杂对象的构建与它的表示分离:即将构建对象的过程解耦出来。试想,如果在产品类中构造和装配一个产品必然需要构造函数来实现,而建造者模式所对应的产品对象是相当复杂的,所以这句话描述的是把本应该出现在产品类中的生产产品的方法抽离出来—— ConcreteBuilder,这样就实现了对象的表示和对象的构建过程分离开来;

  • 同样的构建过程可以创建不同的表示:这句话描述的其实是封装,好比虽然不同的套餐使用的原材料不一样,但是这些套餐都需要同样的产品配件(都是由主食+配菜+小吃+饮料装配而成),所有不同套餐的构建过程是一样的。将这样的生产过程封装起来(通过参数控制套餐的类型),就实现了同样的构建过程产生了不用的套餐。

    代码实现

    Product - 套餐

    1. public abstract class AbstractPackage {
    2. protected String name; // 套餐名
    3. protected String stapleFood; // 主食
    4. protected String sideDish; // 配菜
    5. protected String snack; // 小吃
    6. protected String drinks; // 饮料
    7. public AbstractPackage() {
    8. this.setName();
    9. }
    10. public abstract void setName();
    11. public void setStapleFood(String stapleFood) {
    12. this.stapleFood = stapleFood;
    13. }
    14. public void setSideDish(String sideDish) {
    15. this.sideDish = sideDish;
    16. }
    17. public void setSnack(String snack) {
    18. this.snack = snack;
    19. }
    20. public void setDrinks(String drinks) {
    21. this.drinks = drinks;
    22. }
    23. public void printProject(){
    24. System.out.println(
    25. "套餐信息如下" + '\n' +
    26. " 套餐名:" + name + '\n' +
    27. " 主食:" + stapleFood + '\n' +
    28. " 配菜:" + sideDish + '\n' +
    29. " 小吃:" + snack + '\n' +
    30. " 饮料:" + drinks
    31. );
    32. }
    33. }
    1. public class FlamesPackage extends AbstractPackage {
    2. @Override
    3. public void setName() {
    4. super.name = "烈焰";
    5. }
    6. }
    1. public class IcyPackage extends AbstractPackage {
    2. @Override
    3. public void setName() {
    4. super.name = "冰爽";
    5. }
    6. }

    Builder - 员工工作手册

    1. public abstract class AbstractBuilder {
    2. /**
    3. * 主食
    4. */
    5. public abstract void buildStapleFood();
    6. /**
    7. * 配菜
    8. */
    9. public abstract void buildSideDish();
    10. /**
    11. * 小吃
    12. */
    13. public abstract void buildSnack();
    14. /**
    15. * 饮料
    16. */
    17. public abstract void buildDrinks();
    18. /**
    19. * 返回对象
    20. * @return 套餐对象
    21. */
    22. public abstract AbstractPackage build();
    23. }

    ConcreteBuilder - 商家员工

    1. public class FlamesPackageBuilder extends AbstractBuilder {
    2. private final AbstractPackage pro = new FlamesPackage();
    3. @Override
    4. public void buildStapleFood() {
    5. pro.setStapleFood("辣子鸡套饭");
    6. }
    7. @Override
    8. public void buildSideDish() {
    9. pro.setSideDish("酱菜");
    10. }
    11. @Override
    12. public void buildSnack() {
    13. pro.setSnack("泡椒凤爪");
    14. }
    15. @Override
    16. public void buildDrinks() {
    17. pro.setDrinks("冰红茶");
    18. }
    19. @Override
    20. public AbstractPackage build() {
    21. return this.pro;
    22. }
    23. }
    1. public class IcyPackageBuilder extends AbstractBuilder {
    2. private final AbstractPackage pro = new IcyPackage();
    3. @Override
    4. public void buildStapleFood() {
    5. pro.setStapleFood("薄荷焖饭");
    6. }
    7. @Override
    8. public void buildSideDish() {
    9. pro.setSideDish("泡菜");
    10. }
    11. @Override
    12. public void buildSnack() {
    13. pro.setSnack("凉拌海带丝");
    14. }
    15. @Override
    16. public void buildDrinks() {
    17. pro.setDrinks("雪碧");
    18. }
    19. @Override
    20. public AbstractPackage build() {
    21. return this.pro;
    22. }
    23. }

    Director - 商家老板

    1. public class Director {
    2. private final AbstractBuilder builder;
    3. public Director(AbstractBuilder builder) {
    4. this.builder = builder;
    5. }
    6. public void construct () {
    7. this.builder.buildStapleFood();
    8. this.builder.buildSideDish();
    9. this.builder.buildSnack();
    10. this.builder.buildDrinks();
    11. }
    12. }

    测试代码 - 消费者

    1. public class Client {
    2. public static void main(String[] args) {
    3. System.out.println("|=> 张三点餐 ------------------------------------------|");
    4. AbstractBuilder flamesBuilder = new FlamesPackageBuilder();
    5. new Director(flamesBuilder).construct();
    6. AbstractPackage flames = flamesBuilder.build();
    7. flames.printProject();
    8. System.out.println("|=> 李四点餐 ------------------------------------------|");
    9. AbstractBuilder icyBuilder = new IcyPackageBuilder();
    10. new Director(icyBuilder).construct();
    11. AbstractPackage icy = icyBuilder.build();
    12. icy.printProject();
    13. }
    14. }
    1. |=> 张三点餐 ------------------------------------------|
    2. 套餐信息如下
    3. 套餐名:烈焰
    4. 主食:辣子鸡套饭
    5. 配菜:酱菜
    6. 小吃:泡椒凤爪
    7. 饮料:冰红茶
    8. |=> 李四点餐 ------------------------------------------|
    9. 套餐信息如下
    10. 套餐名:冰爽
    11. 主食:薄荷焖饭
    12. 配菜:泡菜
    13. 小吃:凉拌海带丝
    14. 饮料:雪碧

    建造者模式的扩展

    建造者模式在源码应用中非常广泛,这里还是举两个例子进行说明。

    源码应用举例

    在 jdk 中的应用

    在 jdk 中,StringBuilder 就使用了建造者模式,StringBuilder 作为具体的建造者,继承了抽象的建造者 AbstractStringBuilder 并重写了其中的 append()、delete()、substring()、toString()等方法。StringBuilder 负责生产 String 类型的对象,在构造对象的过程中,可反复调用 append() 等方法,来丰富最终的对象。

    值得一提的是,在 AbstractStringBuilder 的众多方法中,随处可以类似public AbstractStringBuilder append(CharSequence s)这样的方法。调用时,我们可以这样: stringBuilder.append("hello ").append("world ").append("!")... 这就是链式调用,链式调用在开发中的体验爽利,有一种一泻千里的感觉,我个人非常喜欢。

在 Mybatis-Plus 中的应用

在 Mybatis-Plus 的代码生成器中,也应用了建造者模式。类图如下所示
Builder.png
这不是标准的建造者模型,相对于标准的建造者模型来说

  • 缺乏指挥者:指挥者实际上由 FastAutoGenerator 替代了;
  • 建造者抽象接口使用了泛型:相比不使用泛型来说,使用泛型可以不用再写一个抽象产品类出来,在构建时,可直接返回泛型的对象类型,而无需使用抽象产品类;
  • 产品没有抽象产品类:因为使用了泛型 Builder ,无需再定义产品抽象类了。

    灵活使用建造者模式

    分析这样的一个例子:

    经常听到有朋友调侃自己的择偶标准是:有车有房,父母双亡。以此例说明,抽象这个她眼中这个男性朋友,我们能得到包含有如下属性的类:名字、是否有车(默认没有)、是否有房(默认没有)、满意度(这里约定满意度分为不满意,一般,满意,根据是否有房+是否有车而定)等等……

要想构造这样的对象,我们可以考虑使用构造方法,但如果这个对象的属性越来越多,且很多属性有默认值,那么构造方法会越写越多,整个类里充斥着大量的与 对象表示 无关的代码。
此时我们考虑舍弃掉使用构造方法,可以在对象构造完成之后,利用 setter 来构造对象,这是可行的,但对于此例来说仍然不够优雅。因为这里面有一个满意度的属性,这个属性是根据是否有房、是否有车两个字段的内容来决定的,满意度属性的值必须依赖于这两个属性,换句话说,要想知道满意程度必须得先知道是不是有房,是不是有车。那么在 是否有房 和 是否有车 这两个属性的 setter() 方法中均需要设置 满意度 属性的值。

  1. public void setOwnHouse(boolean ownHouse) {
  2. this.ownHouse = ownHouse;
  3. /*
  4. * 可以看到这里有两处同样的逻辑,事实上,因为 setter 方式初始化各个属性存在严格的先后关系,
  5. * 我们无法预知客户端在构造对象时是先调用哪一个 setter,所以只能在两个方法中都设置满意程度
  6. * 属性,但事实上,客户端如果先后设置了这两个属性,那么这两块相同的代码必然有一次执行是没有
  7. * 必要的,多余的
  8. */
  9. if (this.ownCar && this.ownHouse) {
  10. this.satisfactionLevel = "满意";
  11. } else if ((!this.ownCar) && (!this.ownHouse)){
  12. this.satisfactionLevel = "不满意";
  13. } else {
  14. this.satisfactionLevel = "一般";
  15. }
  16. }
  17. public void setOwnCar(boolean ownCar) {
  18. this.ownCar = ownCar;
  19. if (this.ownCar && this.ownHouse) {
  20. this.satisfactionLevel = "满意";
  21. } else if ((!this.ownCar) && (!this.ownHouse)){
  22. this.satisfactionLevel = "不满意";
  23. } else {
  24. this.satisfactionLevel = "一般";
  25. }
  26. }

基于上面的论述,决定采用建造者模式来解决问题,但我们只想利用建造者模式中 对象的构建过程与表示分离 这一特性,对于其他的特性并不关心,此时,我们可以这样来定义类。

  1. public class MaleFriend {
  2. private String name; // 姓名
  3. private boolean ownHouse; // 有房否
  4. private boolean ownCar; // 有车否
  5. private String satisfactionLevel; // 满意程度
  6. public String getName() {
  7. return name;
  8. }
  9. public boolean isOwnHouse() {
  10. return ownHouse;
  11. }
  12. public boolean isOwnCar() {
  13. return ownCar;
  14. }
  15. public String getSatisfactionLevel() {
  16. return satisfactionLevel;
  17. }
  18. public MaleFriend(Builder builder) {
  19. this.name = builder.name;
  20. this.ownHouse = builder.ownHouse;
  21. this.ownCar = builder.ownCar;
  22. if (builder.ownCar && builder.ownHouse) {
  23. this.satisfactionLevel = "满意";
  24. } else if ((!builder.ownCar) && (!builder.ownHouse)){
  25. this.satisfactionLevel = "不满意";
  26. } else {
  27. this.satisfactionLevel = "一般";
  28. }
  29. }
  30. public static class Builder {
  31. private String name;
  32. private boolean ownHouse = false;
  33. private boolean ownCar = false;
  34. public Builder name(String name) {
  35. this.name = name;
  36. return this;
  37. }
  38. public Builder ownHouse(boolean ownHouse) {
  39. this.ownHouse = ownHouse;
  40. return this;
  41. }
  42. public Builder ownCar(boolean ownCar) {
  43. this.ownCar = ownCar;
  44. return this;
  45. }
  46. public MaleFriend build() {
  47. return new MaleFriend(this);
  48. }
  49. }
  50. }

使用时,我们可以用下面的代码来构建对象:

  1. public class Client {
  2. public static void main(String[] args) {
  3. System.out.println("满意类型:");
  4. MaleFriend friend_1 = new MaleFriend.Builder()
  5. .name("张三")
  6. .ownHouse(true)
  7. .ownCar(true)
  8. .build();
  9. print(friend_1);
  10. System.out.println("一般类型:");
  11. MaleFriend friend_2 = new MaleFriend.Builder()
  12. .name("李四")
  13. .ownHouse(true)
  14. .build();
  15. print(friend_2);
  16. System.out.println("不满意类型:");
  17. MaleFriend friend_3 = new MaleFriend.Builder()
  18. .name("王五")
  19. .build();
  20. print(friend_3);
  21. }
  22. private static void print(MaleFriend friend) {
  23. System.out.println(
  24. " 姓名:" + friend.getName() + "\n" +
  25. " 是否有车:" + (friend.isOwnCar() ? "是" : "否") + "\n" +
  26. " 是否有房:" + (friend.isOwnHouse() ? "是" : "否") + "\n" +
  27. " 满意程度:" + friend.getSatisfactionLevel()
  28. );
  29. }
  30. }
  1. 满意类型:
  2. 姓名:张三
  3. 是否有车:是
  4. 是否有房:是
  5. 满意程度:满意
  6. 一般类型:
  7. 姓名:李四
  8. 是否有车:否
  9. 是否有房:是
  10. 满意程度:一般
  11. 不满意类型:
  12. 姓名:王五
  13. 是否有车:否
  14. 是否有房:否
  15. 满意程度:不满意

建造者模式并不是一个死公式,不需要原封不动的套用在各种地方。相反,这应该是一个指导理论,重要的是应该领悟并实践其中的思想,至于如何实现各个要素并处理各个要素之间的关系,应具体问题具体分析。

关于上面这种写法,有一些开发者认为这已经不是建造者模式了,不应该称呼为建造者模式的变种。这里不讨论这个问题,管它叫什么模式,能灵活应用并能实际的解放生产力就是恰当的设计模式。

上面这种“建造者模式变种”的写法也大量出现在了 jdk 、Spring 等的源码中,比如 java.util.Calendar 类,就是这一写法的例子。其内部持有静态内部类 Builder ,Builder 中有多个属性,根据多个不同的方法设置属性的值,在 build() 方法中,根据所有属性的值构造不同的 Calendar 对象。