ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

设计模式之【建造者模式】

2022-03-06 11:34:13  阅读:188  来源: 互联网

标签:int 建造 private water Soup 模式 设计模式 salt public


设计原则是指导我们代码设计的一些经验总结,也就是“心法”;面向对象就是我们的“武器”;设计模式就是“招式”。

以心法为基础,以武器运用招式应对复杂的编程问题。

为什么麦当劳那么受欢迎?

表妹:哥啊,我想吃麦当劳

我:你为啥那么喜欢吃麦当劳呢?

表妹:因为它好吃呀,而且每个门店吃的味道都差不多,不像某县小吃,每个地方吃的味道都有区别。

我:那你知道为什么嘛?

表妹:因为麦当劳有一套非常完善的工作流程,每个门店都必须遵守这套规范...

这不就是我们设计模式中的【建造者模式】嘛?


将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示。

我们以煲汤为例子,我们知道,主汤料和水是煲汤必须的,配料是可放可不放的,有些人喜欢吃原汁原味,可以不放盐,有些肉比较油,那么,也可以不放油。

我们来看看具体的代码实现:

 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://www.cnblogs.com/ChinaHook/p/7471470.html

https://zhuanlan.zhihu.com/p/58093669

标签:int,建造,private,water,Soup,模式,设计模式,salt,public
来源: https://www.cnblogs.com/Gopher-Wei/p/15971144.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有