设计模式之【建造者模式】
设计原则是指导我们代码设计的一些经验总结,也就是“心法”;面向对象就是我们的“武器”;设计模式就是“招式”。
为什么麦当劳那么受欢迎?
表妹:哥啊,我想吃麦当劳
我:你为啥那么喜欢吃麦当劳呢?
表妹:因为它好吃呀,而且每个门店吃的味道都差不多,不像某县小吃,每个地方吃的味道都有区别。
我:那你知道为什么嘛?
表妹:因为麦当劳有一套非常完善的工作流程,每个门店都必须遵守这套规范...
这不就是我们设计模式中的【建造者模式】嘛?
将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示。
我们以煲汤为例子,我们知道,主汤料和水是煲汤必须的,配料是可放可不放的,有些人喜欢吃原汁原味,可以不放盐,有些肉比较油,那么,也可以不放油。
我们来看看具体的代码实现:
1 public class Soup { 2 private static final int MAX_INGREDIENTS = 10; 3 private static final int MAX_OIL = 8; 4 private static final int MAX_SALT = 5; 5 6 private String mainIngredient; // 主料必须 7 private String water; // 水必须 8 private int ingredients; // 配料可选 9 private int oil; // 油可选 10 private int salt; // 盐可选 11 12 public Soup(String main, String water, Integer ingredients, Integer oil, Integer salt) { 13 if (StringUtils.isBlank(main)) { 14 throw new IllegalArgumentException("main should not be empty"); 15 } 16 this.mainIngredient = main; 17 18 if (StringUtils.isBlank(water)) { 19 throw new IllegalArgumentException("water should not be empty"); 20 } 21 this.water = water; 22 23 if (ingredients != null) { 24 if (ingredients < 0) { 25 throw new IllegalArgumentException("ingredients should not be positive"); 26 } 27 this.ingredients = ingredients; 28 } 29 30 if (oil != null) { 31 if (oil < 0) { 32 throw new IllegalArgumentException("oil should not be positive"); 33 } 34 this.oil = oil; 35 } 36 37 if (salt != null) { 38 if (salt < 0) { 39 throw new IllegalArgumentException("salt should not be positive"); 40 } 41 this.salt = salt; 42 } 43 } 44 45 // 省略get方法 46 } 47 ? 48 // 今天想吃鱼头豆腐汤 49 Soup fishHeadTofuSoup = new Soup("鱼头", "山泉水", 10, 6, 3);
大家可以看到,这个构造函数有5个参数,参数列表太长,导致代码在可读性和易用性上都会变差。
而且这么长的参数列表,参数类型一样的都连在一起,在使用构造函数的时候,就很容易搞错各参数的顺序,传递进错误的参数值,导致非常隐蔽的bug。上面这个例子中,配料和盐都是int类型,万一我一不小心,把这两个参数的位置互换了,变成放3克的配料,放10克的盐,那还能吃嘛?
有些同学可能会说,你这个类在构造对象的时候,有些属性是可选可不选的,这些属性可以通过set()函数来设置。
是的,我们来看一下,这种方法的实现效果:
1 public class Soup { 2 private static final int MAX_INGREDIENTS = 10; 3 private static final int MAX_OIL = 8; 4 private static final int MAX_SALT = 5; 5 6 private String mainIngredient; // 主料必须 7 private String water; // 水必须 8 private int ingredients; // 配料可选 9 private int oil; // 油可选 10 private int salt; // 盐可选 11 12 public Soup(String main, String water) { 13 if (StringUtils.isBlank(main)) { 14 throw new IllegalArgumentException("main should not be empty"); 15 } 16 this.mainIngredient = main; 17 18 if (StringUtils.isBlank(water)) { 19 throw new IllegalArgumentException("water should not be empty"); 20 } 21 this.water = water; 22 } 23 24 public void setIngredients(int ingredients) { 25 if (ingredients != null) { 26 if (ingredients < 0) { 27 throw new IllegalArgumentException("ingredients should not be positive"); 28 } 29 this.ingredients = ingredients; 30 } 31 } 32 33 public void setOil(int oil) { 34 if (oil != null) { 35 if (oil < 0) { 36 throw new IllegalArgumentException("oil should not be positive"); 37 } 38 this.oil = oil; 39 } 40 } 41 42 public void setSalt(int salt) { 43 if (salt != null) { 44 if (salt < 0) { 45 throw new IllegalArgumentException("salt should not be positive"); 46 } 47 this.salt = salt; 48 } 49 } 50 51 // 省略get方法 52 } 53 ? 54 // 今天想吃菌菇乌鸡汤 55 Soup blackChickenSoup = new Soup("乌鸡", "水"); 56 blackChickenSoup.setIngredients(8); 57 blackChickenSoup.setOil(5); 58 blackChickenSoup.setSalt(3);
这样一看,确实构造函数的参数列表变短了,也能够煲出美味的汤。但是,还是存在下面几个问题:
1、上面的例子中,只有两个必填的属性,但是如果必填的属性有很多呢?把这些必填属性都放到构造函数中设置,那岂不是又是一个很长的参数列表呢?
2、如果我们把必填属性也通过set()方法设置,那校验这些必填属性是否已经填写的逻辑就无处安放了。
3、如果属性之间有一定的依赖关系,比如,如果煲汤者放了配料ingredients,那么就一定要放盐salt;或者属性之间有一定的约束条件,比如,配料的克数要大于盐的克数。如果我们继续使用现在的设计思路,那这些属性之间的依赖关系或者约束关系的校验逻辑就无处安放了。
4、如果我们希望Soup类对象是不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值了。那么,我们就不能在类中暴露set()方法了。
5、如果这些属性不是一起初始化的话,就会导致对象的无效状态。何为无效状态呢?一起看看下面的代码:
1 Rectangle r = new Rectangle(); // 此时r是无效对象,因为长方形必须有长宽值。 2 r.setWidth(2); // 此时r还是无效状态 3 r.setHeight(4); // 此时的r设置好了长宽值,才是有效状态
这时候,建造者模式就派上用场啦~
建造者模式
1、在Soup类中创建一个静态内部类Builder,然后将Soup类中的参数都复制到Builder类中。
2、在Soup类中创建一个private的构造函数,参数为Builder类型。
3、在Builder中创建一个public的构造函数,参数为Soup中必填的参数,mainIngredient和water。
4、在Builder中创建set()方法,对Soup中那些可选参数进行赋值,返回值为Builder类型的实例。
5、在Builder中创建一个build()方法,在其中创建Soup的实例并返回。
1 public class Soup { 2 private String mainIngredient; 3 private String water; 4 private int ingredient; 5 private int oil; 6 private int salt; 7 8 private Soup(Builder builder) { 9 this.mainIngredient = builder.mainIgredient; 10 this.water = builder.water; 11 this.ingredients = builder.ingredients; 12 this.oil = builder.oil; 13 this.salt = builder.salt; 14 } 15 // 省略get方法 16 17 // 将Builder类设计成Soup的内部类 18 // 也可以将BUilder类设计成独立的非内部类SoupBuilder 19 public static class Builder { 20 private static final int MAX_INGREDIENTS = 10; 21 private static final int MAX_OIL = 8; 22 private static final int MAX_SALT = 5; 23 24 private String mainIngredient; 25 private String water; 26 private int ingredient; 27 private int oil; 28 private int salt; 29 30 public Soup build() { 31 // 校验逻辑放到这里做,包括必填项校验,依赖关系校验,约束条件校验等 32 // 主料必填 33 if (StringUtils.isBlank(mainIngredient)) { 34 throw new IllegalArgumentException("..."); 35 } 36 // 水必填 37 if (StringUtils.isBlank(water)) { 38 throw new IllegalArgumentException("..."); 39 } 40 // 依赖关系:如果放了配料,就一定要放盐 41 if (ingredients > 0 && salt <= 0) { 42 throw new IllegalArgumentException("..."); 43 } 44 // 约束条件:配料的克数不能小于等于盐的克数 45 if (ingredients <= salt) { 46 throw new IllegalArgumentException("..."); 47 } 48 49 return new Soup(this); 50 } 51 52 public Builder setMainIngredients(String mainIngredient) { 53 if (StringUtils.isBlank(mainIngredient)) { 54 throw new IllegalArgumentException("..."); 55 } 56 this.mainIngredient = mainIngredient; 57 return this; 58 } 59 60 public Builder setWater(String water) { 61 if (StringUtils.isBlank(water)) { 62 throw new IllegalArgumentException("..."); 63 } 64 this.water = water; 65 return this; 66 } 67 68 public Builder setIngredients(int ingredients) { 69 if (ingredients != null) { 70 if (ingredients < 0) { 71 throw new IllegalArgumentException("ingredients should not be positive"); 72 } 73 this.ingredients = ingredients; 74 } 75 return this; 76 } 77 78 public Builder setOil(int oil) { 79 if (oil != null) { 80 if (oil < 0) { 81 throw new IllegalArgumentException("oil should not be positive"); 82 } 83 this.oil = oil; 84 } 85 return this; 86 } 87 ? 88 public Builder setSalt(int salt) { 89 if (salt != null) { 90 if (salt < 0) { 91 throw new IllegalArgumentException("salt should not be positive"); 92 } 93 this.salt = salt; 94 } 95 return this; 96 } 97 } 98 } 99 ? 100 // 今天吃冬瓜排骨汤 101 Soup winterMelonRibSoup = new Soup.Builder() 102 .setMainIngredients("排骨") 103 .setWater("山泉水") 104 .setIngredients(8) // 冬瓜8克 105 .setOil(2) 106 .setSalt(3) 107 .builder();
你看,这样设计的话,上面的5个问题都解决了。
大家发现没有,一份美味的汤是由主料、水、配料、油和盐多个简单的对象组成的,然后一步一步构建而成。而建造者模式将变与不变分离,即汤的组成部分是不变的,但每一部分是可以灵活选择的。
像上面煲的冬瓜排骨汤,如果我忘记放油了:
1 Soup winterMelonRibSoup = new Soup.Builder() 2 .setMainIngredients("排骨") // 第一步 3 .setWater("山泉水") 4 .setIngredients(8) // 冬瓜8克 5 // .setOil(2) // 不放油 6 .setSalt(3) 7 .builder();
这样并不会导致状态的无效状态,没有显示设置会自动初始化为默认值。那么,煲出来的味道就不一样了。
可能有人会问,上面定义说“将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示”,但你这也没有实现将构建与表示分离,而且是不同的构建过程创建出不同的表示。
是的,同样的构建过程就相当于麦当劳规范的制作流程,如果按照这套构建过程来构建对象,就不会忘记“放油”了。如下图所示:
其实,上面是Builder在Java中一种简化的使用方式,经典的Builder模式还是有点不同的。
经典Builder模式
它有4个角色:
-
Product:最终要生成的对象,例如Soup实例。
-
Builder:建设者的抽象基类(有时会使用接口代替)。其定义了构建Product的抽象步骤,其实体类需要实现这些步骤。它会包含一个用来返回最终产品的方法Product getProduct()。
-
ConcreteBuilder:Builder的实现类。
-
Director:决定如何构建最终产品的步骤,其会包含一个负责组装的方法void Construct(Builder builder),在这个方法中通过调用builder的方法,就可以设置builder,等设置完成后,就可以通过builder的getProduct()方法获得最终的产品。
接下来我们看一下具体的代码实现:
第一步:我们的目标Soup类:
1 public class Soup { 2 private static final int MAX_INGREDIENTS = 10; 3 private static final int MAX_OIL = 8; 4 private static final int MAX_SALT = 5; 5 6 private String mainIngredient; // 主料必须 7 private String water; // 水必须 8 private int ingredients; // 配料可选 9 private int oil; // 油可选 10 private int salt; // 盐可选 11 12 public Soup(String main, String water) { 13 if (StringUtils.isBlank(main)) { 14 throw new IllegalArgumentException("main should not be empty"); 15 } 16 this.mainIngredient = main; 17 18 if (StringUtils.isBlank(water)) { 19 throw new IllegalArgumentException("water should not be empty"); 20 } 21 this.water = water; 22 } 23 24 public void setIngredients(int ingredients) { 25 if (ingredients != null) { 26 if (ingredients < 0) { 27 throw new IllegalArgumentException("ingredients should not be positive"); 28 } 29 this.ingredients = ingredients; 30 } 31 } 32 33 public void setOil(int oil) { 34 if (oil != null) { 35 if (oil < 0) { 36 throw new IllegalArgumentException("oil should not be positive"); 37 } 38 this.oil = oil; 39 } 40 } 41 42 public void setSalt(int salt) { 43 if (salt != null) { 44 if (salt < 0) { 45 throw new IllegalArgumentException("salt should not be positive"); 46 } 47 this.salt = salt; 48 } 49 } 50 51 // 省略get方法 52 }
第二步:抽象建造者类
1 public abstract class SoupBuilder { 2 public abstract void setIngredients(); 3 public abstract void setSalt(); 4 public abstract void setOil(); 5 6 public abstract Soup getSoup(); 7 }
第三步:实体建造者类,我们可以根据要构建的产品种类产生多个实体建造者类,这里我们构建两种汤,鱼头豆腐汤和冬瓜排骨汤。所以,我们生成了两个实体建造者类。
鱼头豆腐汤建造者类:
1 public class FishHeadTofuSoupBuilder extends SoupBuilder { 2 private Soup soup; 3 public (String mainIngredient, String water) { 4 soup = new Soup(mainIngredient, water); 5 } 6 @Override 7 public void setIngredients() { 8 soup.setIngredients(10); 9 } 10 @Override 11 public void setSalt() { 12 soup.setSalt(3); 13 } 14 @Override 15 public void setOil() { 16 soup.setOil(4); 17 } 18 19 @Override 20 public Soup getSoup() { 21 return soup; 22 } 23 }
冬瓜排骨汤建造者类:
1 public class WinterMelonRibSoupBuilder extends SoupBuilder { 2 private Soup soup; 3 public (String mainIngredient, String water) { 4 soup = new Soup(mainIngredient, water); 5 } 6 @Override 7 public void setIngredients() { 8 soup.setIngredients(7); 9 } 10 @Override 11 public void setSalt() { 12 soup.setSalt(2); 13 } 14 @Override 15 public void setOil() { 16 soup.setOil(3); 17 } 18 19 @Override 20 public Soup getSoup() { 21 return soup; 22 } 23 }
第四步:指导者类(Director)
1 public class SoupDirector { 2 public void makeSoup(SoupBuilder builder) { 3 // 一套规范的流程 4 builder.setIngredients(); 5 builder.setSalt(); 6 builder.setOil(); 7 } 8 }
指导者类SoupDirector在Builder模式中具有很重要的作用,它用于指导具体构建者如何构建产品,控制调用先后顺序。这也就是定义中所说的“使用同样的构建过程”。
那么,我们来看一下,客户端如何构建对象呢?
1 public static void main(String[] args) { 2 SoupDirector director = new SoupDirector(); 3 SoupBuilder builder = new FishHeadTofuSoupBuilder("鱼头", "水"); 4 // 按照那套规范流程来煲汤 5 director.makeSoup(builder); 6 // 鱼头豆腐汤出锅 7 Soup fishHeadTofuSoup = builder.getSoup(); 8 9 // 现在我想喝冬瓜排骨汤 10 SoupBuilder winterMelonRibSoupBuilder = new WinterMelonRibSoupBuilder("排骨", "山泉水"); 11 // 按照那套规范流程来煲汤 12 director.makeSoup(winterMelonRibSoupBuilder); 13 // 冬瓜排骨汤出锅 14 Soup winterMelonRibSoup = winterMelonRibSoupBuilder.getSoup(); 15 }
建造者模式的优点
-
使用建造者模式可以使客户端不必知道产品内部组成的细节。
-
具体的建造者类之间是相互独立的,这有利于系统的扩展。
-
具体的建造者相互独立,因此可以对建造的过程逐步细化,而不会对其他模块产生任何影响。
建造者模式的缺点
-
建造者模式所创建的产品一般具有较多的共同点,其组成部分相似;如果产品之间的差异性很大,则不适合使用建造者模式,因此,其使用范围受到一定的限制。
-
如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大,可维护性降低。
建造者模式与工厂模式的区别
通过前面的学习,我们一起来看看,建造者模式和有什么区别:
-
意图不一样
在工厂方法模式中,我们关注的是一个产品整体,比如tank整体,无需关心tank的各个部分是如何创建出来的;但在建造者模式中,一个具体产品的产生,是依赖各个部件的产生以及装配顺序的,它关注的是“由零件一步一步地组装出产品对象”。简单地说,工厂模式是一个对象创建的粗线条应用,建造者模式则是通过细线条勾勒出一个复杂对象,关注的是产品组成部分的创建过程。
-
产品的复杂度不同
工厂方法模式创建的产品一般都是单一性质的产品,比如tank,都具有一个方法attack(),而建造者模式创建的则是一个复合产品,它由各个部件复合而成,部件不同产品对象当然不同。这不是说工厂方法模式创建的对象简单,而是指它们的粒度大小不同。一般来说,工厂方法模式的对象粒度比较粗,建造者模式的产品对象粒度比较细。
建造者模式的应用场景
当需要创建的产品具备复杂创建过程时,可以抽取出共性创建过程,然后交由具体实现类自定义创建流程,使得同样的创建行为可以生产出不同的产品,分离了创建与表示,使创建产品的灵活性大大增加。
建造者模式主要适用于以下场景:
-
相同的方法,不同的执行顺序,产生不同的结果。
-
多个部件或零件,都可以装配到一个对象中,但是产生的结果又不同。
-
产品类非常复杂,或者产品类中不同的调用顺序产生不同的作用。
-
初始化一个对象比较复杂,参数多,而且很多参数都具有默认值。
总结
建造者模式用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。
参考
极客时间专栏《设计模式之美》
https://zhuanlan.zhihu.com/p/58093669