设计模式之——原型模式

前言

在 Java 中,我们可以使用 new 关键字指定类名来生成类的实例。但是,有的时候,我们也会在不指定类名的前提下生成实例,例如像图形编辑器中拖动现有的模型工具制作图形的实例,这种是非常典型的生成实例的过程太过复杂,很难根据类来生成实例场景,因此需要根据现有的实例来生成新的实例。
像这样根据实例来生成新的实例的模式,我们称之为 原型模式
在软件开发过程中,我们经常会遇到需要创建多个相同或者相似对象的情况,因此 原型模式 的使用频率还是很高的。

定义

定义: 用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。
类型: 类的创建模式

UML图

在原型模式中涉及 PrototypeConcretePrototypeClient 三个角色。
prototype.png
代码如下:

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
/**
* @author zhangguoji
* 2017/8/4 21:46
*/
interface Prototype {
Prototype clone();
}
class ConcretePrototype implements Prototype {
private String attr; // 成员属性
/**
* 克隆方法
*/
@Override
public ConcretePrototype clone() {
// 创建新对象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setAttr(this.attr);
return prototype;
}
@Override
public String toString() {
return "ConcretePrototype[attr=" + attr + "]";
}
public void setAttr(String attr) {
this.attr = attr;
}
public String getAttr() {
return this.attr;
}
}
public class Client {
public static void main(String[] args) {
ConcretePrototype prototype = new ConcretePrototype();
prototype.setAttr("Kevin Zhang");
ConcretePrototype prototype2 = prototype.clone();
System.out.println(prototype.toString());
System.out.println(prototype2.toString());
}
}

输出结果:

1
2
3
4
ConcretePrototype[attr=Kevin Zhang]
ConcretePrototype[attr=Kevin Zhang]
Process finished with exit code 0

Java的原型模式

Java 中使用原型模式很简单, 它为我们提供了复制实例的 clone() 方法。

实际上,所有的 Java 类都继承自 java.lang.Object。Object 类提供一个 clone() 方法,因此,在 Java 中可以直接使用 Object 提供的 clone() 方法来实现对象的克隆。

值得注意的是,被复制对象的类必须实现 java.lang.Cloneable 接口,如果没有实现 java.lang.Cloneable 接口的实例调用了 clone() 方法,会在运行时抛出CloneNotSupportedException 异常。

Cloneable 是一个标记接口,在 Cloneable 接口中并没有声明任何方法,它只是被用来标记可以使用 clone() 方法进行复制。

1
2
public interface Cloneable {
}

案例分析

现在,我有一个消息系统的需求,希望复制原先消息模板进行快速创建,然后可以进行修改后保存。

下面,我们通过 Java 的 clone()方法来实现对象的克隆。

Message 就是具体原型类,复制实现 clone() 方法。
Message代码:

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
/**
* @author zhangguoji
* @date 2017/8/4 21:56
*/
class Message implements Cloneable{
private String author;
private String content;
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
protected Object clone() {
Message message = null;
try {
message = (Message) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return message;
}
@Override
public String toString() {
return "Message{" +
"author='" + author + '\'' +
", content='" + content + '\'' +
'}';
}
}

Client代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author zhangguoji
* @date 2017/8/4 22:00
*/
public class Client {
public static void main(String[] args) {
Message message = new Message();
message.setAuthor("ZGJ");
message.setContent("2017-08-04【消息】");
Message message1 = (Message) message.clone();
message.setContent("2017-08-05【消息】");
System.out.println(message);
System.out.println(message1);
}
}

结果:

1
2
Message{author='ZGJ', content='2017-08-05【消息】'}
Message{author='ZGJ', content='2017-08-04【消息】'}

思考

既然要创建新的实例,为什么不直接使用 new XXX(),而要设计出一个原型模式进行实例的复制呢?

有的时候,我们也会在不指定类名的前提下生成实例,例如像图形编辑器中拖动现有的模型工具制作图形的实例,这种是非常典型的生成实例的过程太过复杂,很难根据类来生成实例场景,因此需要根据现有的实例来生成新的实例。
Prototype原型模式是一种创建型设计模式,它主要面对的问题是:“某些结构复杂的对象”的创建工作;由于需求的变化,这些对象经常面临着剧烈的变化,但是他们却拥有比较稳定一致的接口。

还比如,类初始化需要消化非常多的资源,我们也可以考虑使用原型模式。因为,原型模式是在内存进行二进制流的拷贝,要比直接 new 一个对象性能好很多。

克隆的对象是全新的吗?

原型模式通过 clone() 方法创建的对象是全新的对象,它在内存中拥有新的地址,通过==符号判断返回是flase。

深克隆和浅克隆

clone() 方法使用的是浅克隆。浅克隆对于要克隆的对象, 会复制其基本数据类型和 String 类型或其他final类型的属性的值给新的对象. 而对于非基本数据类型的属性,例如数组、集合, 仅仅复制一份引用给新产生的对象, 即新产生的对象和原始对象中的非基本数据类型的属性都指向的是同一个对象。

此外,还存在深克隆。深克隆对于要克隆的对象, 对于非基本数据类型的属性,例如数组、集合支持复制。换句话说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。

浅克隆和深克隆的主要区别在于,是否支持引用类型的成员变量的复制。

在 Java 中,如果需要实现深克隆,可以通过序列化等方式来实现。
我们在消息中增加一个附件类

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
import java.io.Serializable;
/**
* @author zhangguoji
* @date 2017/8/4 22:10
*/
public class Attachment implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Attachment{" +
"name='" + name + '\'' +
'}';
}
}

在消息中加入这个附件类

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
import java.io.*;
/**
* @author zhangguoji
* 2017/8/4 21:56
*/
class Message implements Serializable {
private static final long serialVersionUID = 1L;
private String author;
private String content;
private Attachment attachment;
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Attachment getAttachment() {
return attachment;
}
public void setAttachment(Attachment attachment) {
this.attachment = attachment;
}
public Message deepClone() throws IOException, ClassNotFoundException {
/**
* 将对象序列化到对象数组流中
*/
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
/**
* 从流中读取对象
*/
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Message) ois.readObject();
}
}

使用序列化实现深度克隆,然后再客户端中测试是否深度克隆成功

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
import java.io.IOException;
/**
* @author zhangguoji
* @date 2017/8/4 22:30
*/
public class Client {
public static void main(String[] args) {
Message message = new Message();
message.setAuthor("ZGJ");
message.setContent("2017-08-04【消息】");
Attachment attachment = new Attachment();
attachment.setName("附件");
message.setAttachment(attachment);
Message message1 = null;
try {
message1 = message.deepClone();
} catch (Exception e) {
e.printStackTrace();
System.out.println("克隆失败");
}
System.out.println("消息是否相同:" + (message == message1));
System.out.println("附件是否相同:" + (message.getAttachment() == message1.getAttachment()));
}
}

结果如下:

1
2
消息是否相同:false
附件是否相同:false

注意

一般而言,Java语言中的clone()方法满足:

  1. 对任何对象x,都有x.clone() != x,即克隆对象与原型对象不是同一个对象;
  2. 对任何对象x,都有x.clone().getClass() == x.getClass(),即克隆对象与原型对象的类型一样;
  3. 如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立。

为了获取对象的一份拷贝,我们可以直接利用Object类的clone()方法,具体步骤如下:

  1. 在派生类中覆盖基类的clone()方法,并声明为public;
  2. 在派生类的clone()方法中,调用super.clone();
    3.派生类需实现Cloneable接口。
    此时,Object类相当于抽象原型类,所有实现了Cloneable接口的类相当于具体原型类。

总结

原型模式的目的在于,根据实例来生成新的实例,我们可以很方便的快速的创建实例。

在 Java 中使用原型模式很简单, 被复制对象的类必须实现 java.lang.Cloneable 接口,并重写 clone() 方法。

使用原型模式的时候,尤其需要注意浅克隆和深克隆问题。在 Java 中,如果需要实现深克隆,可以通过序列化等方式来实现