aoi学院

Aisaka's Blog, School of Aoi, Aisaka University

设计模式-20软工第3周安排-创建型模式-建造者模式、原型模式

师说

我们先说一个生活中的小例子,当我们在外面饭店吃饭时,比如点个水煮肉片,这家店可能会辣一点、那家店可能会咸一点、对面那家可能放青菜、隔壁那家可能放菠菜,每家店做出来的都不一样,明明都是水煮肉片却有不同的做法,如果都一样就不会说这家难吃那家好吃了。

那再看快餐店,比如KFC,我们点个新奥尔良烤鸡腿堡,所有人不管在哪个城市哪家店,做法、味道都是一样的,为什么呢,因为它用料、时间、温度等等都是严格规定的,我们只需要下订单就行了,这就是一个建造者模式。一句话概括就是,将对象复杂的创建过程与表示分离,这样适用同样的构建过程创建不同的对象。

同样地,以我们KFC点餐为例,我们可以点一个汉堡和一个冷饮,汉堡可以是鸡肉汉堡、牛肉汉堡等等,总之是装在盒子中的,冷饮可以是可乐、奶茶等等,反正是装在瓶子中的。下面我们来用建造者模式对其进行组合,用户只需提交订单即可,UML图如下:


也可以看我大三以前的上这门课时的博客:

设计模式-结构型模式-结构型模式概述
设计模式-创建型模式-建造者模式
设计模式-创建型模式-原型模式


建造者模式

简介

建造者模式是属于创建型模式。建造者模式使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
简单的来说就是将一个复杂的东西抽离出来,对外提供一个简单的调用,可以在同样的构建过程创建不同的表示。和工厂模式很相似,不过相比而言更加注重组件的装配。

这里用一个示例来进行说明。

我们一天吃的食物有这些,煎饼、盒饭、拉面、豆浆、牛奶和果汁。分为三餐、早餐、午餐和晚餐,餐点主要包含吃的(俗称饭)和喝的(豆浆,果汁之类的),那么我们可以把煎饼和豆浆作为早餐,盒饭和果汁作为午餐,这样我们可以清楚的知道要吃早餐和午餐包含什么食物。

首先我们定义一个食物类,有两个属性,吃的和喝的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Meal{
private String food;
private String drinks;

public String getFood() {
return food;
}
public void setFood(String food) {
this.food = food;
}

public String getDrinks() {
return drinks;
}
public void setDrinks(String drinks) {
this.drinks = drinks;
}
}

定义了食物时候,我们在定义一个食物的标准接口,一份食物包含什么,其实也就是吃的和喝的。

1
2
3
4
5
interface IBuilderFood{
void buildFood();
void buildDrinks();
Meal createMeal();
}

食物接口定义一个吃的和一个喝的组件,然后通过**createMeal()**方法返回我们需要的食物。
那么现在我们便可以定义一份早餐和午餐。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Breakfast implements IBuilderFood{
Meal meal;

public Breakfast(){
meal=new Meal();
}

@Override
public void buildFood() {
meal.setFood("煎饼");
}

@Override
public void buildDrinks() {
meal.setDrinks("豆浆");
}

@Override
public Meal createMeal() {
return meal;
}
}

class Lunch implements IBuilderFood{
Meal meal;

public Lunch(){
meal=new Meal();
}

@Override
public void buildFood() {
meal.setFood("盒饭");
}

@Override
public void buildDrinks() {
meal.setDrinks("果汁");
}

@Override
public Meal createMeal() {
return meal;
}
}

定义完之后,建造早餐和午餐的的过程已经完毕了。但是这并不是建造者模式,它有个核心的Director(导演者),它用来创建复杂对象的部分,对该部分进行完整的创建或者按照一定的规则进行创建。那么这里我们可以创建一个Director,用来创建一份餐点。至于创建的是什么餐点,它不用知道,这一点由调用者来进行决定。

这里我们就可以定义一个饭店,可以创建一份餐点,创建什么餐点有顾客决定。

1
2
3
4
5
6
7
class FoodStore{
public Meal createBreakfast(IBuilderFood bf){
bf.buildDrinks();
bf.buildFood();
return bf.createMeal();
}
}

创建完成这个Director之后,我们再来进行调用测试。

1
2
3
4
5
6
7
8
9
10
11
public class BuilderTest {

public static void main(String[] args) {
FoodStore foodStore=new FoodStore();
Meal meal=foodStore.createBreakfast(new Breakfast());
Meal meal2=foodStore.createBreakfast(new Lunch());
System.out.println("小明早上吃的是:"+meal.getFood()+",喝的饮料是:"+meal.getDrinks());
System.out.println("小明中午吃的是:"+meal2.getFood()+",喝的饮料是:"+meal2.getDrinks());
}

}

输出结果:

1
2
小明早上吃的是:煎饼,喝的饮料是:豆浆
小明中午吃的是:盒饭,喝的饮料是:果汁

另一种实际更常见的builder方式是类似于这样的:

1
2
3
4
5
Person person = new Person.Builder()
.setName("小明")
.setSex("男")
.setAge("5")
.build();

使用其中的Builder内部类加上内部类其中的set方法,最后build()返回外部类实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class Person {

/**
* 性别
*/
private final String sex;
/**
* 年龄
*/
private final String age;
/**
* 名字
*/
private final String name;

public String getName() {
return name;
}

public String getAge() {
return age;
}

public String getSex() {
return sex;
}

public Person(String sex, String age, String name) {
this.sex = sex;
this.age = age;
this.name = name;
}

public static final class Builder {

private String sex;
private String age;
private String name;

public Builder() {

}

public Builder setSex(String sex) {
this.sex = sex;
return this;
}

public Builder setAge(String age) {
this.age = age;
return this;
}

public Builder setName(String name) {
this.name = name;
return this;
}

public Person build() {
return new Person(this.sex, this.age, this.name);
}
}

}
1
2
3
4
5
6
7
8
9
10
11
12
public class Test {

public static void main(String[] args) {

Person person = new Person.Builder()
.setName("小明")
.setSex("男")
.setAge("5")
.build();

}
}

如果一个对象构造参数很多的时候,比如10个以上,优势就体现出来了。

当然,这只是建造者模式的简单使用。即使是再复杂的使用方式,代码模板还是 Builder类 + build() 方法形式。

简单的介绍了下建造者模式的运作原理,可以概况为这4点:

  • Builder:指定一个抽象的接口,规定该产品所需实现部件的创建,并不涉及具体的对象部件的创建。
  • ConcreteBuilder:需实现Builder接口,并且针对不同的逻辑,进行不同方法的创建,最终提供该产品的实例。
  • Director:用来创建复杂对象的部分,对该部分进行完整的创建或者按照一定的规则进行创建。
  • Product:示被构造的复杂对象。

使用场景:
适用一些基本组件不便,但是组合经常变化的时候。比如超市促销的大礼包。

优点:

  • 建造者独立,易扩展。
  • 便于控制细节风险。

缺点:

  • 内部结构复杂,不易于理解。
  • 产品直接需要有共同点,范围有控制。

原型模式

原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

一般来说我们在创建对象的时候是直接创建的,但是创建该对象的代价很大的时候,重复的二次创建就有些不划算,这时我们就可以使用原型模式。
打个比方,我们都发送过邮件,在节日的时候一般发送的是祝福语句,在这些祝福语句中,一般除了名字不一样之外,大部分都是一样的。这时我们就可以利用该模式来进行相应出创建。

这里还是用一个的简单的示例来说明:
小明和小红在同一天生日,然后我们需要给他们发送邮件进行祝福,但是由于比较懒,祝福语除了名字之外都是一样的。这时我们就可以先完成祝福语的编写,然后克隆该祝福语,最后根据不同的名称进行发送。不过这里就从简了,只是简单的打印下而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class PrototypeTest {

public static void main(String[] args) {
Mail mail=new Mail();
mail.setMsg("生日快乐!");
Mail mail2=(Mail) mail.clone();
mail.setName("小明");
mail2.setName("小红");
System.out.println(mail.toString());
System.out.println(mail2.toString());
}
}

class Mail implements Cloneable {
private String name;
private String msg;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}
public Object clone() {
Object clone = null;
try {
clone = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}

@Override
public String toString() {
return name + ":" + msg ;
}

}

输出结果:

1
2
小明:生日快乐!
小红:生日快乐!

看完原型模式的创建,是不是感觉就是和Java中克隆即为类似呢?
实际上它的核心也就是克隆。
克隆有两种,浅克隆和深克隆,本文主要介绍的是浅克隆。

浅克隆:

在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。
简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。
实现Cloneable接口并重写Object类中的clone()方法。

深克隆:

在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。
简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。
实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。

使用场景:

  • 类初始化的时候需要消耗大量资源的时候
  • 获取数据库连接繁琐的时候
  • 一个对象,有很多个修改者的时候

优点:

  • 可以提升性能

缺点:

  • 因为必须实现 Cloneable 接口,所以用起来可能不太方便

参考资料

20软工第3周安排
Java进阶篇设计模式之三 —– 建造者模式和原型模
对工厂模式与建造者模式的个人理解,以及结合运用
面试必考点之建造者模式