设计模式之建造者模式和原型模式(深/浅复制问题)

背景

建造者模式和原型模式属于创建型模式,建造者模式和工厂模式有相似之处,他们都可以动态获得不同产品的类型,这些具体产品都来源于一个产品父类,而建造者模式更加注重产品内部组件的装配过程。原型模式则涉及到对象的深复制和浅复制的问题。

建造者模式

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

建造者模式又称生成器模式,可以将一个产品的内部表象与产品的生成过程分割开来,从而使一个建造过程生成具有不同内部表象的产品对象。如果我们用了建造者模式,那么用户只需指定建造的类型就可以得到它们,而具体的建造细节就不需要知道了。

举个例子,玩RPG游戏时要创建一个角色然后开始探险。角色有很多类型:战士,射手,法师,刺客等等。他们都有一些相似的共有属性,比如,用什么武器,应该穿什么防具,初始血量、法力值等等。所以,创建这些角色的过程都大体相似,依次设置这些共有属性的值。对于玩家而言,他们只需要在创建角色页面选择自己要玩的角色,并不关心后台是如何生成这些角色的。

定义一个角色类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Character {
String weapon;
String guard;

public String getWeapon() {
return weapon;
}

public void setWeapon(String weapon) {
this.weapon = weapon;
}

public String getGuard() {
return guard;
}

public void setGuard(String guard) {
this.guard = guard;
}

public void show() {
System.out.println("这个角色的武器是:" + weapon + ",防具精通:" + guard);
}
}

角色建造器接口

1
2
3
4
5
6
7
interface CharacterBuilder {
void buildWeapon();

void buildGuard();

Character createCharacter();
}

这个接口规定了建造器需要设置角色的武器和防具,具体怎么设置,则交给子类具体的建造器。

具体角色的建造器

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
class SwordsmanBuilder implements CharacterBuilder {
Character character = new Character();

@Override
public void buildWeapon() {
character.setWeapon("残破的剑");
}

@Override
public void buildGuard() {
character.setGuard("重甲");
}

@Override
public Character createCharacter() {
return character;
}
}

class GunnerBuilder implements CharacterBuilder {
Character character = new Character();

@Override
public void buildWeapon() {
character.setWeapon("没子弹的枪");
}

@Override
public void buildGuard() {
character.setGuard("皮甲");
}

@Override
public Character createCharacter() {
return character;
}
}

还有一个建造者模式核心的类,Director。用来指挥具体的建造过程,至于建造的是什么角色,它不用关心,由调用者决定。

1
2
3
4
5
6
7
class CharacterPanel {
public Character createCharacter(CharacterBuilder characterBuilder) {
characterBuilder.buildWeapon();
characterBuilder.buildGuard();
return characterBuilder.createCharacter();
}
}

主程序

1
2
3
4
5
6
7
8
9
public class Builder {
public static void main(String[] args) {
CharacterPanel characterPanel = new CharacterPanel();
Character swordsman = characterPanel.createCharacter(new SwordsmanBuilder());
Character gunner = characterPanel.createCharacter(new GunnerBuilder());
swordsman.show();
gunner.show();
}
}

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

  • Builder:指定一个抽象的接口,规定该产品所需实现部件的创建,并不涉及具体的对象部件的创建。

  • ConcreteBuilder:需实现Builder接口,并且针对不同的逻辑,进行不同方法的创建,最终提供该产品的实例。

  • Director:用来创建复杂对象的部分,对该部分进行完整的创建或者按照一定的规则进行创建。

  • Product:示被构造的复杂对象。

什么时候使用建造者模式?
当要创建一些复杂的对象,这些对象内部构建间的建造顺序通常是稳定的,但对象内部的构建通常面临着复杂的变化时。

优点:
使得建造代码与表示代码分离,由于建造者隐藏了该产品是如何组装的,所以若需要改变一个产品的内部表示,只需要再定义一个具体的建造者ConcreteBuilder就可以了。

原型模式

描述:用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象。
原型模式
原型模式是从一个对象再创建另外一个可定制的对象,而且不需要知道任何创建的细节。
举个简历的例子:简历上有各自个人信息,其中工作经历这个信息又可以包含更具体的信息,如工作时间,工作地点等,因此可以封装成一个类。

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
class Resume implements Serializable, Cloneable {
String name;
String sex;
String age;
WorkExperience workExperience;

public Resume(String name) {
this.name = name;
workExperience = new WorkExperience();
}

public void setPersonalInfo(String sex, String age) {
this.sex = sex;
this.age = age;
}

public void setWorkExperience(String workDate, String company) {
workExperience.workDate = workDate;
workExperience.company = company;
}

@Override
public String toString() {
return "Resume{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", age='" + age + '\'' +
", workExperience=" + workExperience +
'}';
}

@Override
protected Object clone() throws CloneNotSupportedException {
Resume resume = (Resume) super.clone();
if (workExperience != null) {
resume.workExperience = (WorkExperience) workExperience.clone();
}
return resume;
}
}

class WorkExperience implements Serializable, Cloneable {
String workDate;
String company;

@Override
public String toString() {
return "WorkExperience{" +
"workDate='" + workDate + '\'' +
", company='" + company + '\'' +
'}';
}

@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

如果现在我想要很多份简历,这些简历除了工作经历属性可以因投递公司的不同而有详有略外,其他信息都一样。如果每创建一份简历都要new一次,都要执行一次构造函数,假如构造函数的执行时间很长,那么多次执行构造函数这个初始化操作的效率就很低了。 这时候就可以使用原型模式,实际上java有个Cloneable接口,重写clone()方法即可实现原型模式。原型模式的基础实现可自行了解。

主程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Prototype {
public static void main(String[] args) {
Resume resume1 = new Resume("bxy");
resume1.setPersonalInfo("male", "21");
resume1.setWorkExperience("2017-2021", "NUIST");

System.out.println(resume1.toString());
try {
Resume resume2 = (Resume) resume1.clone();
resume2.setWorkExperience("2017-2019","A公司");

Resume resume3 = (Resume) resume1.clone();
resume3.setWorkExperience("2017-2018","B公司");

} catch (Exception e){
e.printStackTrace();
}
}
}

使用场景:
当需要创建多个对象,而这些对象有很多初始化信息都不变。
原型模式的好处是不用重新初始化对象,而是动态地获得对象运行时的状态。

既然原型模式是为了克隆对象而生的,一个对象的属性有基本类型(值类型)和非基本类型(引用类型)。对于非基本类型属性的复制就引出了深复制和浅复制的问题。

浅复制

创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。

在上述例子中,WorkExperience是非基本属性,在Resume对象中存的是引用。浅复制的Resume2和原来的Resume1中的WorkExperience都指向同一个对象。

深复制

创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。

在一些特定的场合,我们会需要深复制。即希望Resume2和Resume1中的WorkExperience指向不同的对象。为了做到这点,可以让待复制对象的非基本类型的对象也实现Cloneable接口,也可以使用对象流将对象写出流再读出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//深复制实现1
@Override
protected Object clone() throws CloneNotSupportedException {
Resume resume = (Resume) super.clone();
if (workExperience != null) {
resume.workExperience = (WorkExperience) workExperience.clone();
}
return resume;
}

//深复制实现2
ByteArrayOutputStream byteOut=new ByteArrayOutputStream();
ObjectOutputStream objOut=new ObjectOutputStream(byteOut);
objOut.writeObject(resume1);

ByteArrayInputStream byteIn=new ByteArrayInputStream(byteOut.toByteArray());
ObjectInputStream objIn=new ObjectInputStream(byteIn);
Resume resume3=(Resume)objIn.readObject();
System.out.println(resume1.workExperience == resume3.workExperience);

在选择深复制方法时,应根据对象的复杂程度,如引用类型属性是否有多层引用类型属性关系。如果对象只有一层或者两层引用类型的属性,让引用类型的对象也实现Cloneable接口较为方便,反之则使用对象流。

参考资料:

0%