原型模式

1 实际场景

1.1 订单处理系统

考虑实现一个订单处理系统,为了方便订单的后续处理,系统需要在订单保存的时候,每当订单的预定产品数量超过 1000 时,就需要将订单拆成两份订单来保存,若订单拆分后,数量仍然超过 1000,则持续拆分,直到每份订单的预定产品数量不超过 1000

其中根据当前业务,订单的类型分为两种:一种是个人订单;一种是公司订单;

1.2 不使用设计模式的方案

1、定义订单接口

要实现通用的订单处理,不用关心具体的订单类型,则订单处理的对象应该面向一个订单的接口或者是一个通用的订单对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @author mark.hct
* @version OrderApi.java v 0.1 2023/2/9 23:27 Exp $
* @description 订单的接口
*/
public interface OrderApi {
/**
* 获取订单产品数量
*
* @return 订单中产品数量
*/
public int getOrderProductNum();

/**
* 设置订单产品数量
*
* @param num 订单产品数量
*/
public void setOrderProductNum(int num);
}

2、个人订单

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
/**
* @author mark.hct
* @version PersonalOrder.java v 0.1 2023/2/9 23:27 Exp $
* @description 个人订单对象
*/
public class PersonalOrder implements OrderApi {
/**
* 订购人员姓名
*/
private String customerName;
/**
* 产品编号
*/
private String productId;
/**
* 订单产品数量
*/
private int orderProductNum = 0;

public int getOrderProductNum() {
return this.orderProductNum;
}

public void setOrderProductNum(int num) {
this.orderProductNum = num;
}

public String getCustomerName() {
return customerName;
}

public void setCustomerName(String customerName) {
this.customerName = customerName;
}

public String getProductId() {
return productId;
}

public void setProductId(String productId) {
this.productId = productId;
}

public String toString() {
return "本个人订单的订购人是=" + this.customerName + ",订购产品是=" + this.productId + ",订购数量为=" + this.orderProductNum;
}
}

3、企业订单

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 mark.hct
* @version EnterpriseOrder.java v 0.1 2023/2/9 23:29 Exp $
* @description 企业订单对象
*/
public class EnterpriseOrder {
/**
* 企业名称
*/
private String enterpriseName;
/**
* 产品编号
*/
private String productId;
/**
* 订单产品数量
*/
private int orderProductNum = 0;

public int getOrderProductNum() {
return this.orderProductNum;
}

public void setOrderProductNum(int num) {
this.orderProductNum = num;
}

public String getEnterpriseName() {
return enterpriseName;
}

public void setEnterpriseName(String enterpriseName) {
this.enterpriseName = enterpriseName;
}

public String getProductId() {
return productId;
}

public void setProductId(String productId) {
this.productId = productId;
}

public String toString() {
return "本企业订单的订购企业是=" + this.enterpriseName + ",订购产品是=" + this.productId + ",订购数量为=" + this.orderProductNum;
}

}

注:

以上代码主要示意作用,实际场景远比这复杂,为了方便演示,未抽象共同父类

4、实现通用的订单处理

先定义出订单处理的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author mark.hct
* @version OrderBusiness.java v 0.1 2023/2/9 23:35 Exp $
* @description 处理订单的业务对象
*/
public class OrderBusiness {
/**
* 创建订单的方法
*
* @param order 订单的接口对象
*/
public void saveOrder(OrderApi order) {
//等待具体实现
}
}

实现拆分订单逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @author mark.hct
* @version OrderBusiness.java v 0.1 2023/2/9 23:35 Exp $
* @description 处理订单的业务对象
*/
public class OrderBusiness {
/**
* 创建订单的方法
*
* @param order 订单的接口对象
*/
public void saveOrder(OrderApi order) {
//1:判断当前的预定产品数量是否大于1000
while (order.getOrderProductNum() > 1000) {
//2:如果大于,还需要继续拆分
//2.1再新建一份订单,跟传入的订单除了数量不一样外,其它都相同
OrderApi newOrder = null;

///...
}
}
}

其中,拆分订单时,需要将一个订单拆分为多个,则需要新建相应的订单对象,但是由于其中传入的参数是订单接口,则无法确定订单的具体类型,所以无法创建对象,进而无法实现订单的拆分功能。

5、粗暴的方法

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
/**
* @author mark.hct
* @version OrderBusiness.java v 0.1 2023/2/9 23:35 Exp $
* @description 处理订单的业务对象
*/
public class OrderBusiness {
/**
* 创建订单的方法
*
* @param order 订单的接口对象
*/
public void saveOrder(OrderApi order) {
//根据业务要求,当订单预定产品数量超过1000时,就要把订单拆成两份订单
//当然如果要做好,这里的1000应该做成常量,这么做是为了演示简单

//1:判断当前的预定产品数量是否大于1000
while (order.getOrderProductNum() > 1000) {
//2:如果大于,还需要继续拆分
//2.1再新建一份订单,跟传入的订单除了数量不一样外,其它都相同
OrderApi newOrder = null;
if (order instanceof PersonalOrder) {
//创建相应的新的订单对象
PersonalOrder p2 = new PersonalOrder();
//然后进行赋值,但是产品数量为1000
PersonalOrder p1 = (PersonalOrder) order;
p2.setCustomerName(p1.getCustomerName());
p2.setProductId(p1.getProductId());
p2.setOrderProductNum(1000);
//然后再设置给newOrder
newOrder = p2;
} else if (order instanceof EnterpriseOrder) {
//创建相应的订单对象
EnterpriseOrder e2 = new EnterpriseOrder();
//然后进行赋值,但是产品数量为1000
EnterpriseOrder e1 = (EnterpriseOrder) order;
e2.setEnterpriseName(e1.getEnterpriseName());
e2.setProductId(e1.getProductId());
e2.setOrderProductNum(1000);
//然后再设置给newOrder
newOrder = (OrderApi) e2;
}

//2.2原来的订单保留,把数量设置成减少1000
order.setOrderProductNum(order.getOrderProductNum() - 1000);

//然后是业务功能处理,省略了,打印输出,看一下
System.out.println("拆分生成订单==" + newOrder);
}
//3:不超过1000,那就直接业务功能处理,省略了,打印输出,看一下
System.out.println("订单==" + order);
}
}

6、客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @author mark.hct
* @version OrderClient.java v 0.1 2023/2/9 23:56 Exp $
* @description
*/
public class OrderClient {
public static void main(String[] args) {
//创建订单对象,这里为了演示简单,直接new了
PersonalOrder op = new PersonalOrder();
//设置订单数据
op.setOrderProductNum(2925);
op.setCustomerName("张三");
op.setProductId("P0001");

//这里获取业务处理的类,也直接new了,为了简单,连业务接口都没有做
OrderBusiness ob = new OrderBusiness();
//调用业务来保存订单对象
ob.saveOrder(op);
}
}

运行结果:

1
2
3
拆分生成订单==本个人订单的订购人是=张三,订购产品是=P0001,订购数量为=1000
拆分生成订单==本个人订单的订购人是=张三,订购产品是=P0001,订购数量为=1000
订单==本个人订单的订购人是=张三,订购产品是=P0001,订购数量为=925

1.3 存在的问题

在上面的实现中,看似完成了需求且没有关心订单的具体类型。但事实上,在实现订单处理的时候,上面的实现是按照订单的类型和具体实现来处理的,就是 instanceof 的那一段。会导致几个问题:

  • 既然想要实现通用的订单处理,那么对于订单处理的实现对象,是不应该知道订单的具体实现的,更不应该依赖订单的具体实现。但是上面的实现中,很明显订单处理的对象依赖了订单的具体实现对象。
  • 难以扩展新的订单类型。假如现在要加入一个大客户专用订单的类型,那么就需要修改订单处理的对象,要在里面添加对新的订单类型的支持,代码无法适配。

2 解决方案

2.1 原型模式

2.1.1 定义

原型模式(Prototype)是创建型模式:用原型实例指定创建对象的种类,并通过拷贝原型来创建新对象。

2.1.2 原型模式解决方案思路

通过分析上述案例,在方法 saveOrder 中,已经有了订单接口类型的对象实例,是从接口的参数传入的,但是参数封装的是订单的接口类型,并不是具体的实现类型。现在的需求是,需在在 saveOrder 方法中创建订单对象,看起来就像是通过接口的方法来创建。

原型模式就可以解决此类问题:原型模式通过要求对象实现一个可以克隆自身的接口,这样就可以通过拷贝或者克隆一个实例对象本身,来达到创建一个新的实例。使用时只需要调用克隆的接口方法,使用上看就像是通过接口来创建了一个新的对象。
如此,通过原型实例创建新的对象,而无需关注这个实例本身的类型,也不用关心对象的具体实现,只要它实现了克隆自身的方法,就可以通过这个方法来获取新的对象,而无须再去通过 new 来创建。

2.1.3 结构说明

image-20230210173431732
  • Prototype:声明一个克隆自身的接口,用来约束想要克隆自己的类,要求它们都要实现这里定义的克隆方法。
  • ConcretePrototype:实现 Prototype 接口的类,这些类真正实现了克隆自身的功能。
  • Client:使用原型的客户端,首先要获取到原型实例对象,然后通过原型实例克隆自身来创建新的对象实例。

2.1.4 实例代码

1、原型接口定义

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author mark.hct
* @version Prototype.java v 0.1 2023/2/10 18:37 Exp $
* @description 声明一个克隆自身的接口
*/
public interface Prototype {
/**
* 克隆自身的方法
*
* @return 一个从自身克隆出来的对象
*/
Prototype clone();
}

2、具体原型实现对象

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
/**
* @author mark.hct
* @version ConcretePrototype1.java v 0.1 2023/2/10 18:37 Exp $
* @description 克隆的具体实现对象
*/
public class ConcretePrototype1 implements Prototype {
@Override
public Prototype clone() {
//最简单的克隆,新建一个自身对象,由于没有属性,就不去复制值了
Prototype prototype = new ConcretePrototype1();
return prototype;
}
}

/**
* @author mark.hct
* @version ConcretePrototype2.java v 0.1 2023/2/10 19:44 Exp $
* @description 克隆的具体实现对象
*/
public class ConcretePrototype2 implements Prototype {
@Override
public Prototype clone() {
//最简单的克隆,新建一个自身对象,由于没有属性,就不去复制值了
Prototype prototype = new ConcretePrototype2();
return prototype;
}
}

注:

为了示例方便,这两个具体的原型实现对象,都没有定义属性。事实上,在实际使用原型模式的应用中,原型对象多是有属性的,克隆原型的时候也是需要克隆原型对象的属性

3、客户端

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
/**
* @author mark.hct
* @version Client.java v 0.1 2023/2/10 18:45 Exp $
* @description 使用原型的客户端
*/
public class Client {
/**
* 持有需要使用的原型接口对象
*/
private Prototype prototype;

/**
* 构造方法,传入需要使用的原型接口对象
*
* @param prototype 需要使用的原型接口对象
*/
public Client(Prototype prototype) {
this.prototype = prototype;
}

/**
* 示意方法,执行某个功能操作
*/
public void operation() {
//会需要创建原型接口的对象
Prototype newPrototype = prototype.clone();
}
}

2.2 重写方案

使用原型模式来重写示例,先要在订单的接口上定义出克隆的接口,然后要求各个具体的订单对象克隆自身,这样就可以解决:在订单处理对象里面通过订单接口来创建新的订单对象的问题。

通过接口来强制要求对象有复制的方法,利用这个方法实现对象对自己的复制。

1、订单接口

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
/**
* @author mark.hct
* @version OrderApi.java v 0.1 2023/2/13 13:33 Exp $
* @description 订单的接口
*/
public interface OrderApi {
/**
* 获取订单产品数量
*
* @return 订单中产品数量
*/
int getOrderProductNum();

/**
* 设置订单产品数量
*
* @param num 订单产品数量
*/
void setOrderProductNum(int num);

/**
* 拷贝方法
*
* @return 订单原型的实例
*/
OrderApi cloneOrder();
}

注:当前例子中,已经有订单的接口存在,就无需重新创建一个原型接口来声明克隆接口;

2、订单对象定义

克隆拷贝对象时要求将数据也要复制

个人订单:

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
/**
* @author mark.hct
* @version PersonalOrder.java v 0.1 2023/2/13 13:35 Exp $
* @description 个人订单对象
*/
public class PersonalOrder implements OrderApi {
/**
* 订购人员姓名
*/
private String customerName;
/**
* 产品编号
*/
private String productId;
/**
* 订单产品数量
*/
private int orderProductNum = 0;

public int getOrderProductNum() {
return this.orderProductNum;
}

public void setOrderProductNum(int num) {
this.orderProductNum = num;
}

public String getCustomerName() {
return customerName;
}

public void setCustomerName(String customerName) {
this.customerName = customerName;
}

public String getProductId() {
return productId;
}

public void setProductId(String productId) {
this.productId = productId;
}

public String toString() {
return "本个人订单的订购人是=" + this.customerName + ",订购产品是=" + this.productId + ",订购数量为=" + this.orderProductNum;
}

@Override
public OrderApi cloneOrder() {
//创建一个新的订单,然后把本实例的数据复制过去
PersonalOrder order = new PersonalOrder();
order.setCustomerName(this.customerName);
order.setProductId(this.productId);
order.setOrderProductNum(this.orderProductNum);

return order;
}
}

企业订单:

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
/**
* @author mark.hct
* @version EnterpriseOrder.java v 0.1 2023/2/13 13:36 Exp $
* @description 企业订单对象
*/
public class EnterpriseOrder implements OrderApi {
/**
* 企业名称
*/
private String enterpriseName;
/**
* 产品编号
*/
private String productId;
/**
* 订单产品数量
*/
private int orderProductNum = 0;

public int getOrderProductNum() {
return this.orderProductNum;
}

public void setOrderProductNum(int num) {
this.orderProductNum = num;
}

public String getEnterpriseName() {
return enterpriseName;
}

public void setEnterpriseName(String enterpriseName) {
this.enterpriseName = enterpriseName;
}

public String getProductId() {
return productId;
}

public void setProductId(String productId) {
this.productId = productId;
}

public String toString() {
return "本企业订单的订购企业是=" + this.enterpriseName + ",订购产品是=" + this.productId + ",订购数量为=" + this.orderProductNum;
}

@Override
public OrderApi cloneOrder() {
//创建一个新的订单,然后把本实例的数据复制过去
EnterpriseOrder order = new EnterpriseOrder();
order.setEnterpriseName(this.enterpriseName);
order.setProductId(this.productId);
order.setOrderProductNum(this.orderProductNum);
return order;
}
}

3、订单处理对象

这里使用订单接口的克隆方法,是订单的处理对象,也就是说,订单的处理对象就相当于原型模式结构中的 Client

当然,客户端在调用 clone 方法之前,还需要先获得相应的实例对象,有了实例对象,才能调用该实例对象的 clone 方法。

这里使用克隆方法,跟标准的原型实现有一些不同,在标准的原型实现的示例代码里面,客户端是持有需要克隆的对象,而这里是通过方法传入需要使用克隆的对象

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
/**
* @author mark.hct
* @version OrderBusiness.java v 0.1 2023/2/13 13:37 Exp $
* @description 处理订单的业务对象
*/
public class OrderBusiness {
/**
* 创建订单的方法
*
* @param order 订单的接口对象
*/
public void saveOrder(OrderApi order) {
//1:判断当前的预定产品数量是否大于1000
while (order.getOrderProductNum() > 1000) {
//2:如果大于,还需要继续拆分
//2.1再新建一份订单,跟传入的订单除了数量不一样外,其它都相同
OrderApi newOrder = order.cloneOrder();
//然后进行赋值,产品数量为1000
newOrder.setOrderProductNum(1000);

//2.2原来的订单保留,把数量设置成减少1000
order.setOrderProductNum(order.getOrderProductNum() - 1000);

//然后是业务功能处理,省略了,打印输出,看一下
System.out.println("拆分生成订单==" + newOrder);
}
//3:不超过,那就直接业务功能处理,省略了,打印输出,看一下
System.out.println("订单==" + order);
}
}

注:

虽然 Java 里面有 clone 方法,上面这么做还是很有意义的,可以让我们更好的、更完整的体会原型这个设计模式。关于 Javaclone 方法,后续会讲解。

3 模式讲解

3.1 认识模型模式

模型模式的功能实际包含两个功能:

  1. 通过克隆(clone)的方法来创建新的对象实例;
  2. 对克隆出来的新对象实例复制原实例属性的值;

一般来讲,新创建出来的实例的数据是和原型实例一样的。但是具体如何实现克隆,需要由代码自行实现,原型模式并没有统一的要求和实现算法。

克隆(clone)new 对象的区别:

  • 克隆类似于 new,但是不等于 new
  • new 一个对象实例,一般属性是没有值的,或者是只有默认值;
  • 如果是克隆得到的一个实例,通常属性是有值的,属性的值就是原型对象实例在克隆的时候,原型对象实例的属性的值

所以,通过原型模式生成的新对象实例,实际上与原来的对象实例是没有关联的,即,对象实例在不同的内存空间。

3.2 Java 中的拷贝

Java 中已经提供了 clone 的接口方法,定义在 Object 类中,利用 Javaclone 方法来实现上面的案例。

1、订单接口定义

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
/**
* @author mark.hct
* @version OrderApi.java v 0.1 2023/2/13 20:33 Exp $
* @description
*/
public interface OrderApi {
/**
* 获取订单产品数量
*
* @return 订单中产品数量
*/
int getOrderProductNum();

/**
* 设置订单产品数量
*
* @param num 订单产品数量
*/
void setOrderProductNum(int num);

///**
// * 拷贝方法
// *
// * @return 订单原型的实例
// */
//pattern.prototype.renew.OrderApi cloneOrder();
}

2、实现 java.lang.Cloneable 接口的订单对象,这里就实现一个类作为示例

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
/**
* @author mark.hct
* @version PersonalOrder.java v 0.1 2023/2/13 20:37 Exp $
* @description
*/
public class PersonalOrder implements OrderApi, Cloneable {
/**
* 订购人员姓名
*/
private String customerName;
/**
* 产品编号
*/
private String productId;
/**
* 订单产品数量
*/
private int orderProductNum = 0;

public int getOrderProductNum() {
return this.orderProductNum;
}

public void setOrderProductNum(int num) {
this.orderProductNum = num;
}

public String getCustomerName() {
return customerName;
}

public void setCustomerName(String customerName) {
this.customerName = customerName;
}

public String getProductId() {
return productId;
}

public void setProductId(String productId) {
this.productId = productId;
}

public String toString() {
return "本个人订单的订购人是=" + this.customerName + ",订购产品是=" + this.productId + ",订购数量为=" + this.orderProductNum;
}

//@Override
//public pattern.prototype.renew.OrderApi cloneOrder() {
// //创建一个新的订单,然后把本实例的数据复制过去
// pattern.prototype.renew.PersonalOrder order = new pattern.prototype.renew.PersonalOrder();
// order.setCustomerName(this.customerName);
// order.setProductId(this.productId);
// order.setOrderProductNum(this.orderProductNum);
//
// return order;
//}

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

3、客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @author mark.hct
* @version Client.java v 0.1 2023/2/13 20:40 Exp $
* @description
*/
public class Client {

public static void main(String[] args) throws CloneNotSupportedException {
//先创建原型实例
PersonalOrder oa1 = new PersonalOrder();
//设置原型实例的订单数量的值
oa1.setOrderProductNum(100);
System.out.println("这是第一次获取的对象实例===" + oa1.getOrderProductNum());

//通过克隆来获取新的实例
PersonalOrder oa2 = (PersonalOrder) oa1.clone();
oa2.setOrderProductNum(80);
System.out.println("输出克隆出来的实例===" + oa2.getOrderProductNum());
//再次输出原型实例的值
System.out.println("再次输出原型实例===" + oa1.getOrderProductNum());
}
}

运行结果:

1
2
3
这是第一次获取的对象实例===100
输出克隆出来的实例===80
再次输出原型实例===100

3.3 浅拷贝与深拷贝

前面提到了 clone,正好引申一下浅拷贝和深拷贝的概念和区别。

  • 浅拷贝:只负责拷贝按值传递的数据(比如:基本数据类型、String 类型)
  • 深拷贝:除了浅拷贝要拷贝的值外,还负责拷贝引用类型的数据,基本上就是被拷贝实例所有的属性的数据都会被克隆出来

注:深拷贝还有一个特点,如果被克隆的对象里面的属性数据是引用类型,也就是属性的类型也是对象,那么需要一直递归的克隆下去。这也意味着,要想深拷贝成功,必须要整个克隆所涉及的对象都要正确实现克隆方法,如果其中有一个没有正确实现克隆,那么就会导致深拷贝失败。

上一小结使用的就是浅拷贝。

深拷贝案例

1 不使用 Java 的 Cloneble 方式

继续上面的案例,实现一个引用对象给订单对象

1、订单对象的属性所引用的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author mark.hct
* @version ProductPrototype.java v 0.1 2023/2/13 19:48 Exp $
* @description 声明一个克隆产品自身的接口
*/
public interface ProductPrototype {
/**
* 克隆产品自身的方法
*
* @return 一个从自身克隆出来的产品对象
*/
ProductPrototype cloneProduct();
}
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 mark.hct
* @version Product.java v 0.1 2023/2/13 20:49 Exp $
* @description 产品对象
*/
public class Product implements ProductPrototype {
/**
* 产品编号
*/
private String productId;
/**
* 产品名称
*/
private String name;

public String getName() {
return name;
}

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

public String getProductId() {
return productId;
}

public void setProductId(String productId) {
this.productId = productId;
}

public String toString() {
return "产品编号=" + this.productId + ",产品名称=" + this.name;
}

public ProductPrototype cloneProduct() {
//创建一个新的订单,然后把本实例的数据复制过去
Product product = new Product();
product.setProductId(this.productId);
product.setName(this.name);
return product;
}
}

2、新的订单对象实现,增加的上面实现的引用对象

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
/**
* @author mark.hct
* @version PersonalOrder.java v 0.1 2023/2/13 20:50 Exp $
* @description
*/
public class PersonalOrder implements OrderApi {
private String customerName;
private int orderProductNum = 0;
/**
* 产品对象
*/
private Product product = null;

public int getOrderProductNum() {
return this.orderProductNum;
}

public void setOrderProductNum(int num) {
this.orderProductNum = num;
}

public String getCustomerName() {
return customerName;
}

public void setCustomerName(String customerName) {
this.customerName = customerName;
}

public Product getProduct() {
return product;
}

public void setProduct(Product product) {
this.product = product;
}

public String toString() {
//简单点输出
return "订购产品是=" + this.product.getName() + ",订购数量为=" + this.orderProductNum;
}

public OrderApi cloneOrder() {
//创建一个新的订单,然后把本实例的数据复制过去
PersonalOrder order = new PersonalOrder();
order.setCustomerName(this.customerName);
order.setOrderProductNum(this.orderProductNum);
//对于对象类型的数据,深度克隆的时候需要继续调用这个对象的克隆方法
order.setProduct((Product) this.product.cloneProduct());
return order;
}
}

3、客户端代码 - 验证深拷贝

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
/**
* @author mark.hct
* @version Client.java v 0.1 2023/2/13 19:51 Exp $
* @description
*/
public class Client {
public static void main(String[] args) {
//先创建原型实例
PersonalOrder oa1 = new PersonalOrder();
//设置原型实例的值
Product product = new Product();
product.setName("产品1");
oa1.setProduct(product);
oa1.setOrderProductNum(100);

System.out.println("这是第一次获取的对象实例=" + oa1);

//通过克隆来获取新的实例
PersonalOrder oa2 = (PersonalOrder) oa1.cloneOrder();
//修改它的值
oa2.getProduct().setName("产品2");
oa2.setOrderProductNum(80);
//输出克隆出来的对象的值
System.out.println("输出克隆出来的实例=" + oa2);

//再次输出原型实例的值
System.out.println("再次输出原型实例=" + oa1);
}
}

运行结果:

1
2
3
这是第一次获取的对象实例=订购产品是=产品1,订购数量为=100
输出克隆出来的实例=订购产品是=产品2,订购数量为=80
再次输出原型实例=订购产品是=产品1,订购数量为=100

深拷贝的实现不复杂,但是比较麻烦,比如多个对象的属性都是引用类型的话,需要循环去实现为 clone 方法,并且每个都要正确实现此方法,如果有一个遗漏,都会引起深拷贝的报错。

2 使用 Java 的 Cloneable 方式

1、新增产品类的实现,主要将 ProductPrototype 接口改成 Cloneable 接口实现

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
/**
* @author mark.hct
* @version Product.java v 0.1 2023/2/13 21:01 Exp $
* @description
*/
public class Product implements Cloneable {
private String productId;
private String name;

public String getName() {
return name;
}

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

public String getProductId() {
return productId;
}

public void setProductId(String productId) {
this.productId = productId;
}

public String toString() {
return "产品编号=" + this.productId + ",产品名称=" + this.name;
}

public Object clone() {
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return obj;
}
}

2、订单类的实现

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
/**
* @author mark.hct
* @version PersonalOrder.java v 0.1 2023/2/13 20:02 Exp $
* @description
*/
public class PersonalOrder implements OrderApi, Cloneable {
private String customerName;
private Product product = null;
private int orderProductNum = 0;

public int getOrderProductNum() {
return this.orderProductNum;
}

public void setOrderProductNum(int num) {
this.orderProductNum = num;
}

public String getCustomerName() {
return customerName;
}

public void setCustomerName(String customerName) {
this.customerName = customerName;
}

public Product getProduct() {
return product;
}

public void setProduct(Product product) {
this.product = product;
}

public String toString() {
//简单点输出
return "订购产品是=" + this.product.getName() + ",订购数量为=" + this.orderProductNum;
}

public Object clone() {
PersonalOrder obj = null;
try {
obj = (PersonalOrder) super.clone();
//下面这一句话不可少
obj.setProduct((Product) this.product.clone());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return obj;
}
}

特别注意:

不可缺少 obj.setProduct((Product)this.product.clone());

原因在于调用 super.clone() 方法的时候,Java 是先开辟一块内存的空间,然后把实例对象的值原样拷贝过去,对于基本数据类型这样做是没有问题的,而属性 product 是一个引用类型,把值拷贝过去的意思就是把对应的内存地址拷贝过去了,也就是说克隆后的对象实例的 product 和原型对象实例的 product 指向的是同一块内存空间,是同一个产品实例。

因此要想正确的执行深度拷贝,必须手工的对每一个引用类型的属性进行克隆,并重新设置,覆盖掉 super.clone() 所拷贝的值

3.4 原型管理器

有了原型管理器后,一般情况下,除了向原型管理器里面添加原型对象的时候是通过 new 来创造的对象,其余时候都是通过向原型管理器来请求原型实例,然后通过克隆方法来获取新的对象实例,这就可以实现动态管理、或者动态切换具体的实现对象实例。

在很多实际应用场景中,系统的原型数据是不固定的,其中原型是可以被动态的创建和销毁,这种情况下,就需要在系统中维护一个当前可用的原型注册表,这个注册表称为原型管理器

若原型是一个资源的话, 此时原型管理器就相当于是一个资源管理器,系统启动时,管理器就初始化,然后运行期间可以动态的添加资源和销毁资源,类似于一个资源缓存池。

案例说明

1、定义原型接口

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @author mark.hct
* @version Prototype.java v 0.1 2023/2/13 22:04 Exp $
* @description
*/
public interface Prototype {
Prototype clone();

String getName();

void setName(String name);
}

2、原型实现

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
/**
* @author mark.hct
* @version ConcretePrototype1.java v 0.1 2023/2/13 22:05 Exp $
* @description
*/
public class ConcretePrototype1 implements Prototype {
private String name;

@Override
public Prototype clone() {
ConcretePrototype1 prototype = new ConcretePrototype1();
prototype.setName(this.name);
return prototype;
}

@Override
public String getName() {
return name;
}

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

@Override
public String toString() {
return "Now in Prototype1,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
/**
* @author mark.hct
* @version ConcretePrototype2.java v 0.1 2023/2/13 22:09 Exp $
* @description
*/
public class ConcretePrototype2 implements Prototype {
private String name;

@Override
public Prototype clone() {
ConcretePrototype2 prototype2 = new ConcretePrototype2();
prototype2.setName(this.name);
return prototype2;
}

@Override
public String getName() {
return name;
}

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

@Override
public String toString() {
return "Now in Prototype2,name=" + name;
}
}

3、原型管理器

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
/**
* @author mark.hct
* @version PrototypeManager.java v 0.1 2023/2/13 22:12 Exp $
* @description 原型管理器
*/
public class PrototypeManager {
/**
* 用来记录原型的编号和原型实例的对应关系
*/
private static Map<String, Prototype> map = new HashMap<String, Prototype>();

/**
* 私有化构造方法,避免外部无谓的创建实例
*/
private PrototypeManager() {
//
}

/**
* 向原型管理器里面添加或是修改某个原型注册
*
* @param prototypeId 原型编号
* @param prototype 原型实例
*/
public synchronized static void setPrototype(String prototypeId, Prototype prototype) {
map.put(prototypeId, prototype);
}

/**
* 从原型管理器里面删除某个原型注册
*
* @param prototypeId 原型编号
*/
public synchronized static void removePrototype(String prototypeId) {
map.remove(prototypeId);
}

/**
* 获取某个原型编号对应的原型实例
*
* @param prototypeId 原型编号
* @return 原型编号对应的原型实例
* @throws Exception 如果原型编号对应的原型实例不存在,报出例外
*/
public synchronized static Prototype getPrototype(String prototypeId) throws Exception {
Prototype prototype = map.get(prototypeId);
if (prototype == null) {
throw new Exception("您希望获取的原型还没有注册或已被销毁");
}
return prototype;
}
}

原型管理器是类似一个工具类的实现方式,而且对外的几个方法都是加了同步的,这主要是因为如果在多线程环境下使用这个原型管理器的话,map 属性容易成为竞争的资源,因此需要加上同步

4、客户端

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
/**
* @author mark.hct
* @version Client.java v 0.1 2023/2/13 22:23 Exp $
* @description
*/
public class Client {
public static void main(String[] args) {
try {
// 初始化原型管理器
Prototype p1 = new ConcretePrototype1();
PrototypeManager.setPrototype("Prototype1", p1);

// 获取原型来创建对象
Prototype p3 = PrototypeManager.getPrototype("Prototype1").clone();
p3.setName("张三");
System.out.println("第一个实例:" + p3);

// 有人动态的切换了实现
Prototype p2 = new ConcretePrototype2();
PrototypeManager.setPrototype("Prototype1", p2);

// 重新获取原型来创建对象
Prototype p4 = PrototypeManager.getPrototype("Prototype1").clone();
p4.setName("李四");
System.out.println("第二个实例:" + p4);

// 有人注销了这个原型
PrototypeManager.removePrototype("Prototype1");

// 再次获取原型来创建对象
Prototype p5 = PrototypeManager.getPrototype("Prototype1").clone();
p5.setName("王五");
System.out.println("第三个实例:" + p5);
} catch (Exception err) {
System.err.println(err.getMessage());
}
}
}

运行结果:

1
2
3
第一个实例:Now in Prototype1,name=张三
第二个实例:Now in Prototype2,name=李四
您希望获取的原型还没有注册或已被销毁

3.5 原型模式的优缺点

1、对客户端隐藏具体的实现类型

原型模式的客户端,只知道原型接口的类型,并不知道具体的实现类型,从而减少了客户端对这些具体实现类型的依赖。

2、在运行时动态改变具体的实现类型

原型模式可以在运行期间,有客户端来注册符合原型接口实现类型,也可同台的改变具体的实现类型,看起来接口没有任何变化,但是其实运行的已经是另外一个类的实例了,因为克隆一个原型就类似于实例化一个类。

3、深拷贝实现比较麻烦

原型模式最大的缺点就在于每个原型的子类都必须实现 clone 的操作,尤其在包含引用类型的对象时,clone 方法会比较麻烦,必须要能够递归的让所有的相关对象都要正确的实现克隆。

3.6 思考

1、原型模式的本质

本质:克隆生成对象,克隆是手段,目的还是生成新的对象实例。

原型模式也可以用来解决 “只知接口而不知实现的问题”,使用原型模式,可以出现一种独特的 “接口造接口” 的景象,这在面向接口编程中很有用。同样的功能也可以考虑使用工厂来实现。

需要注意的是,原型模式的重心还是在创建新的对象实例,至于创建出来的对象,其属性的值是否一定要和原型对象属性的值完全一样,这个并没有强制规定,在大多数实现中,克隆出来的对象和原型对象的属性值是一样的。

2、使用场景

  • 当一个系统想要独立于它想要使用的对象时,可以使用原型模式,系统只需要面向接口编程,在系统需要新的对象时,通过克隆原型来得到;
  • 当需要实例化的类是在运行时刻动态指定时

3.7 相关模式

1、原型模式和抽象工厂模式

功能上有些相似,都是用来获取一个新的对象实例。不同之处在于:

  • 原型模式主要关注如何创建出示例对象,选择的方案是克隆;
  • 抽象模式主要关注的如何创建产品簇,并不关注具体如何创建出产品簇中的每个对象实例;

2、原型模式和生成器模式

两种模式可以配合使用。生成器模式关注的是构建的过程,在构建过程中,可能需要某个部件的实例,此时可以搭配原型模式使用,通过原型模式来得到部件的实例。