设计模式03-原型模式与建造者模式

原型模式

原型模式(Prototype Pattern)是指通过原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。调用者不需要知道任何创建细节,不调用构造函数。原型模式属于创建型模式。

适用场景

  • 类初始化消耗资源较多
  • new创建一个对象需要繁琐的过程(数据准备、访问权限等)
  • 构造函数比较复杂
  • 循环体中产生大量对象时,可读性下降

示例代码

浅克隆

一个标准的原型模式代码设计过程应该是这样的,

先创建原型接口:

1
2
3
public interface ProtoType {
ProtoType clone();
}

创建需要具体克隆的对象ConcreteProtoTypeA:

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
public class ConcreteProtoTypeA implements ProtoType {

private String name;
private int age;
private List<String> hobbies;

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public List<String> getHobbies() {
return hobbies;
}

public void setHobbies(List<String> hobbies) {
this.hobbies = hobbies;
}

@Override
public ProtoType clone() {
ConcreteProtoTypeA concreteProtoType = new ConcreteProtoTypeA();
concreteProtoType.setName(this.name);
concreteProtoType.setAge(this.age);
concreteProtoType.setHobbies(this.hobbies);
return concreteProtoType;
}

}

创建Client对象:

1
2
3
4
5
public class Client {
public ProtoType startClone(ProtoType concreteProtoType) {
return (ProtoType) concreteProtoType.clone();
}
}

测试类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ProtoTypeTest {
public static void main(String[] args) {
//需要克隆的对象
ConcreteProtoTypeA concreteProtoType = new ConcreteProtoTypeA();
concreteProtoType.setName("protoType");
concreteProtoType.setAge(30);
List<String> hobbies = new ArrayList<>();
concreteProtoType.setHobbies(hobbies);
System.out.println(concreteProtoType);

//模拟客户端调用
Client client = new Client();
ConcreteProtoTypeA concreteProtoTypeClone = (ConcreteProtoTypeA) client.startClone(concreteProtoType);
System.out.println(concreteProtoTypeClone);

//从结果可看出,引用地址相同,复制的是引用的地址,而不是值
System.out.println("原对象中引用类型地址: " + concreteProtoType.getHobbies());
System.out.println("克隆对象中引用类型地址: " + concreteProtoTypeClone.getHobbies());
System.out.println("对象地址比较: " + (concreteProtoType.getHobbies() == concreteProtoTypeClone.getHobbies()));

}
}

输出结果:

1
2
3
4
5
com.example.designpattern.prototype.shallow.ConcreteProtoTypeA@54a097cc
com.example.designpattern.prototype.shallow.ConcreteProtoTypeA@5a61f5df
原对象中引用类型地址: []
克隆对象中引用类型地址: []
对象地址比较: true

从测试结果可知getHobbies()的引用地址是相同的,即复制的是引用地址而不是值。这种情况下,如果修改任意一个对象的属性值,所有引用对象的属性值都会改变(所有的引用对象仍指向原来的对象),即浅克隆,下面使用深度克隆改造。

深克隆

创建原型Monkey类:

1
2
3
4
5
public class Monkey {
public int height;
public int weight;
public Date birthday;
}

创建引用类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Stick implements Serializable {
public float h = 100;
public float d = 10;

public void big() {
this.h *= 2;
this.d *= 2;
}

public void small() {
this.h /= 2;
this.d /= 2;
}
}

创建具体对象MonkeyKing类:

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
public class MonkeyKing extends Monkey implements Cloneable, Serializable {
public Stick stick;

public MonkeyKing() {
this.birthday = new Date();
this.stick = new Stick();
}

@Override
protected Object clone() {
return this.deepClone();
}

public Object deepClone() {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);

ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);

MonkeyKing copy = (MonkeyKing) ois.readObject();
copy.birthday = new Date();
return copy;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

public MonkeyKing shallowClone(MonkeyKing target) {
MonkeyKing monkeyKing = new MonkeyKing();
monkeyKing.height = target.height;
monkeyKing.weight = target.weight;
monkeyKing.stick = target.stick;
monkeyKing.birthday = new Date();
return monkeyKing;
}
}

测试类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DeepCloneTest {
public static void main(String[] args) {
//原型对象
MonkeyKing monkeyKing = new MonkeyKing();
try {
//深拷贝对象
MonkeyKing deepClone = (MonkeyKing) monkeyKing.clone();
System.out.println("深拷贝:" + (deepClone.stick == monkeyKing.stick));
} catch (Exception e) {
e.printStackTrace();
}

MonkeyKing monkeyKing1 = new MonkeyKing();
//浅拷贝
MonkeyKing shallowClone = monkeyKing1.shallowClone(monkeyKing1);
System.out.println("浅拷贝:" + (shallowClone.stick == monkeyKing1.stick));
}
}

运行结果:

1
2
深拷贝:false
浅拷贝:true

克隆破坏单例模式

如果深度克隆的目标是单例对象,那么就会破坏单例。要解决深度克隆破坏单例的问题,可以考虑禁止深度克隆

  • 单例类不实现Cloneable接口
  • 重写clone()方法,在clone方法中返回单例对象

代码实现如下:

1
2
3
4
@Override
protected Object clone() throws CloneNotSupportedException {
return INSTANCE;
}

Cloneable源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Returns a shallow copy of this {@code ArrayList} instance. (The
* elements themselves are not copied.)
*
* @return a clone of this {@code ArrayList} instance
*/
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}

JDK实例

  • BeanUtils.copy()
  • arrayList.clone()

模式优点

  • 原型模式比直接new一个对象性能高
  • 简化了创建过程

模式缺点

  • 必须配备克隆方法
  • 对克隆复杂对象或对克隆出来的对象进行复杂改造时,容易带来风险
  • 需要合理使用深拷贝、浅拷贝

建造者模式

建造者模式(Builder Pattern)是将一个复杂对象的构建过程与它的表示分离,使得同样的构建过程可以创建不同的表示,属于创建型模式。使用建造者模式对于用户而言,只需指定需要建造的类型就可以获得对象,建造过程及细节不需要了解。

建造者模式适用于创建对象需要很多步骤,但步骤的顺序不一定固定。如果一个对象有非常复杂的内部结构,可以将复杂对象的创建和使用进行分离。类图如下:

建造者模式的设计中主要有四个角色:

  1. 产品(Product),要创建的产品类对象
  2. 抽象的建造者(Builder),建造者的抽象类,规范产品对象的各个组成部分的建造,一般由子类实现具体的建造过程
  3. 具体的建造者(ConcreteBuilder),具体的Builder类,根据不同的业务逻辑,具体化对象的各个组成部分的创建
  4. 调用者(Director),调用具体的创建者来创建对象的各个部分,在调用者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建

适用场景

建造者模式适用于一个具有较多的零件的复杂产品的创建过程,由于需求的变化,组成这个复杂产品的各个零部件经常变化,但他们的组合方式却相对稳定。适用场景包括:

  1. 相同的方法,不同的执行顺序,产生不同的结果时
  2. 多个部件可以装配到一个对象中,但是产生的结果又不同
  3. 产品类非常复杂或者产品类中的调用顺序不同产生不同的作用
  4. 当初始化一个对象很复杂,有很多参数且大多有默认值

示例代码

https://github.com/chenpenghui93/tutorial-design-pattern

源码示例

JDK中的StringBuilder,MyBatis中的CacheBuilder、SqlSessionFactoryBuilder

模式优点

  • 封装性好,创建和使用分离
  • 扩展性好,建造类之间独立、一定程度上解耦

模式缺点

  • 产生多余的Builder对象
  • 产品内部发生变化,建造者都要修改,成本较大

建造者模式与工厂模式对比:

  1. 建造者模式更加注重方法的调用顺序,工厂模式注重创建对象
  2. 创建对象的力度不同,建造者模式创建复杂的对象,由各种复杂的部件组成,工厂模式创建出来的都一样
  3. 关注重点不一样,建造者模式不仅要创建对象,还要知道对象由哪些部件组成;工厂模式只需要把对象创建出来就可以
  4. 建造者模式根据建造过程中的顺序不一样,最终的对象部件组成也不一样

参考