原型模式 (Prototype)
场景问题
问题案例
考虑这样一个实际应用:订单处理系统。
现在有一个订单处理的系统,里面有一个保存订单的业务功能。在这个业务功能中,客户有这样一个需求:每当订单的预定产品数量超过1000的时候,就需要把订单拆成两份订单来保存。如果拆成两份订单后,还是超过1000,那就继续拆分,直到每份订单的预定产品数量不超过1000。至于为什么要拆分,原因是方便进行订单的后续处理,后续是由人工来处理,每个人工工作小组的处理能力上限是1000。
根据业务,目前的订单类型被分成两种:一种是个人订单,一种是公司订单。现在想要实现一个通用的订单处理系统,也就是说,不管具体是什么类型的订单,都要能够正常地处理。
该怎么实现呢?
不用模式的解决方案
来分析上面要求实现的功能,有朋友会想,这很简单嘛,一共就一个功能,没什么困难的,真的是这样吗?下面来尝试着实现看看。 (1)定义订单接口。 首先,要想实现通用的订单处理,而不关心具体的订单类型,那么很明显,订单处理的对象应该面向一个订单的接口或是一个通用的订单对象来编程,这里就选用面向订单的接口来处理。先把这个订单接口定义出来。示例代码如下:
/**
* 订单的接口
*/
public interface OrderApi {
/**
* 获取订单产品数量
* @return 订单中产品数量
*/
public int getOrderProductNum();
/**
* 设置订单产品数量
* @param num 订单产品数量
*/
public void setOrderProductNum(int num);
}
/**
* 个人订单对象
*/
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;
}
}
/**
* 企业订单对象
*/
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;
}
}
/**
* 处理订单的业务对象
*/
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 = e2;
}
//2.2原来的订单保留,把数量设置成减少1000
order.setOrderProductNum(order.getOrderProductNum()-1000);
//然后是业务功能处理,省略了,打印输出,看一下
System.out.println("拆分生成订单=="+newOrder);
}
//3:不超过,那就直接业务功能处理,省略了,打印输出,看一下
System.out.println("订单=="+order);
}
}
客户端订单处理的代码如下:
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);
}
}
运行结果如下:
拆分生成订单==本个人订单的订购人是=张三,订购产品是=P0001,订购数量为=1000
拆分生成订单==本个人订单的订购人是=张三,订购产品是=P0001,订购数量为=1000
订单==本个人订单的订购人是=张三,订购产品是=P0001,订购数量为=925
根据订单中订购产品的数量,一份订单被拆分成了三份。
同样的,你还可以传入企业订单,看看是否能正常满足功能要求。
有何问题
看起来,上面的实现确实不难,好像也能够通用地进行订单处理,而不需要关心订单的类型和具体实现这样的功能。
仔细想想,真的没有关心订单的类型和具体实现吗?答案是“否定的”。
事实上,在实现订单处理的时候,上面的实现是按照订单的类型和具体实现来处理的,就是instanceof的那一段。有朋友可能会问,这样实现有何不可吗?
这样的实现有以下几个问题。
- 既然想要实现通用的订单处理,那么对于订单处理的实现对象,是不应该知道订单的具体实现的,更不应该依赖订单的具体实现。但是上面的实现中,很明显订单处理的对象依赖了订单的具体实现对象。
- 这种实现方式另外一个问题就是:难以扩展新的订单类型。假如现在要加入一个大客户专用订单的类型,那么就需要修改订单处理的对象,要在里面添加对新的订单类型的支持,这不能算做通用处理。
因此,上面的实现是不太好的,把上面的问题再抽象描述一下:已经有了某个对象实例后,如何能够快速简单地创建出更多的这种对象?
比如上面的问题,就是已经有了订单接口类型的对象实例,然后在方法中需要创建出更多的这种对象。怎么解决呢?”
解决方案
原型模式的定义
用来解决上述问题的一个合理的解决方案就是原型模式(Prototype)。那么什么是原型模式呢?
-
原型模式的定义
用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。
-
应用原型模式来解决问题的思路
仔细分析上面的问题,在saveOrder方法里面,已经有了订单接口类型的对象实例,是从外部传入的,但是这里只是知道这个实例对象的种类是订单的接口类型,并不知道其具体的实现类型,也就是不知道它到底是个人订单还是企业订单,但是现在需要在这个方法里面创建一个这样的订单对象,看起来就像是要通过接口来创建对象一样。
原型模式就可以解决这样的问题。原型模式会要求对象实现一个可以“克隆”自身的接口,这样就可以通过拷贝或者是克隆一个实例对象本身来创建一个新的实例。如果把这个方法定义在接口上,看起来就像是通过接口来创建了新的接口对象。
这样一来,通过原型实例创建新的对象,就不再需要关心这个实例本身的类型,也不关心它的具体实现,只要它实现了克隆自身的方法,就可以通过这个方法来获取新的对象,而无须再去通过new来创建。
原型模式的结构
- Prototype:声明一个克隆自身的接口,用来约束想要克隆自己的类,要求它们都要实现这里定义的克隆方法。
- ConcretePrototype:实现Prototype接口的类,这些类真正实现了克隆自身的功能。
- Client:使用原型的客户端,首先要获取到原型实例对象,然后通过原型实例克隆自身来创建新的对象实例。
原型模式的实现
/**
* 声明一个克隆自身的接口
*/
public interface Prototype {
/**
* 克隆自身的方法
* @return 一个从自身克隆出来的对象
*/
public Prototype clone();
}
/**
* 克隆的具体实现对象
*/
public class ConcretePrototype1 implements Prototype {
public Prototype clone() {
//最简单的克隆,新建一个自身对象,由于没有属性,就不去复制值了
Prototype prototype = new ConcretePrototype1();
return prototype;
}
}
/**
* 克隆的具体实现对象
*/
public class ConcretePrototype2 implements Prototype {
public Prototype clone() {
//最简单的克隆,新建一个自身对象,由于没有属性,就不去复制值了
Prototype prototype = new ConcretePrototype2();
return prototype;
}
}
使用原型模式的客户端的代码如下:
/**
* 使用原型的客户端
*/
public class Client {
/**
* 持有需要使用的原型接口对象
*/
private Prototype prototype;
/**
* 构造方法,传入需要使用的原型接口对象
* @param prototype 需要使用的原型接口对象
*/
public Client(Prototype prototype){
this.prototype = prototype;
}
/**
* 示意方法,执行某个功能操作
*/
public void operation(){
//会需要创建原型接口的对象
Prototype newPrototype = prototype.clone();
}
}
原型重写示例
示例结构
要使用原型模式来重写示例,先要在订单的接口上定义出克隆的接口,然后要求各个具体的订单对象克隆自身,这样就可以解决:在订单处理对象里面通过订单接口来创建新的订单对象的问题。
示例代码
下面一起来看看具体的实现。
-
复制谁和谁来复制的问题
有了一个对象实例,要快速地创建和它一样的实例,最简单的办法就是复制?这里又有两个小的问题:
-
复制谁呢?当然是复制这个对象实例,复制实例的意思是连带着数据一起复制。
-
谁来复制呢?应该让这个类的实例自己来复制,自己复制自己。
可是每个对象不会那么听话,自己去实现复制自己的。于是原型模式决定对这些对象实行强制要求,给这些对象定义一个接口,在接口里面定义一个方法,这个方法用来要求每个对象实现自己复制自己。
由于现在存在订单的接口,因此就把这个要求克隆自身的方法定义在订单的接口里面。示例代码
/*** 订单的接口,声明了可以克隆自身的方法*/public interface OrderApi {/*** 获取订单产品数量* @return 订单中产品数量*/public int getOrderProductNum();/*** 设置订单产品数量* @param num 订单产品数量*/public void setOrderProductNum(int num);/*** 克隆方法* @return 订单原型的实例*/public OrderApi cloneOrder();} -
-
如何克隆
定义好了克隆的接口,那么在订单的实现类里面,就得让它实现这个接口,并具体地实现这个克隆方法。新的问题出来了,如何实现克隆呢?
很简单,只要先new一个自己对象的实例,然后把自己实例中的数据取出来,设置到新的对象实例中去,就可以完成实例的复制,复制的结果就是有了一个同自身一模一样的实例。
有的朋友可能会说:不用那么费劲吧,直接返回本实例不就可以了?例如:
public Object clone() {return this;}请注意,这是一种典型的错误,这么做,每次克隆,客户端获取的其实都是同一个实例,都是指向同一个内存空间的,对克隆出来的对象实例的修改会影响到原型对象实例。
那么应该怎么克隆呢?最基本的做法就是新建一个类实例,然后把所有属性的值复制到新的实例中。
/*** 个人订单对象*/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;}public OrderApi cloneOrder() {//创建一个新的订单,然后把本实例的数据复制过去PersonalOrder order = new PersonalOrder();order.setCustomerName(this.customerName);order.setProductId(this.productId);order.setOrderProductNum(this.orderProductNum);return order;}}接下来看看企业订单的具体实现。示例代码如下:
/*** 企业订单对象*/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;}public OrderApi cloneOrder() {//创建一个新的订单,然后把本实例的数据复制过去EnterpriseOrder order = new EnterpriseOrder();order.setEnterpriseName(this.enterpriseName);order.setProductId(this.productId);order.setOrderProductNum(this.orderProductNum);return order;}} -
使用克隆方法
这里使用订单接口的克隆方法的是订单的处理对象,也就是说,订单的处理对象就相当于原型模式结构中的Client。
当然,客户端在调用clone方法之前,还需要先获得相应的实例对象,有了实例对象,才能调用该实例对象的clone方法。
这里使用克隆方法的时候,和标准的原型实现有一些不同,在标准的原型实现的示例代码里面,客户端是持有需要克隆的对象,而这里变化成了通过方法传入需要使用克隆的对象,这点大家注意一下。
示例代码如下:
/*** 处理订单的业务对象*/public class OrderBusiness {/*** 创建订单的方法* @param order 订单的接口对象*/public void saveOrder(OrderApi order){//根据业务要求,当订单的预定的产品数量超过1000的时候,就需要把订单拆成两份订单//当然如果要做好,这里的1000应该做成常量,这么做是为了演示简单//1:判断当前的预定产品数量是否大于1000while(order.getOrderProductNum() > 1000){//2:如果大于,还需要继续拆分//2.1再新建一份订单,跟传入的订单除了数量不一样外,其他都相同OrderApi newOrder = order.cloneOrder();//然后进行赋值,产品数量为1000newOrder.setOrderProductNum(1000);//2.2原来的订单保留,把数量设置成减少1000order.setOrderProductNum(order.getOrderProductNum()-1000);//然后是业务功能处理,省略了,打印输出,看一下System.out.println("拆分生成订单=="+newOrder);}//3:不超过,那就直接业务功能处理,省略了,打印输出,看一下System.out.println("订单=="+order);}}客户端执行代码
public class OrderClient {public static void main(String[] args) {//创建订单对象,这里为了演示简单,直接new了PersonalOrder op = new PersonalOrder();//设置订单数据op.setOrderProductNum(3925);op.setCustomerName("张三");op.setProductId("P0001");//这里获取业务处理的类,也直接new了,为了简单,连业务接口都没有做OrderBusiness ob = new OrderBusiness();//调用业务来保存订单对象ob.saveOrder(op);}}
在上面的例子中,在订单处理对象的保存订单方法里面的这句话“OrderApi newOrder=order.cloneOrder();”,就用一个订单的原型实例来指定了对象的种类,然后通过克隆这个原型实例来创建出了一个新的对象实例。
看到这里,可能有些朋友会认为:Java的Object里面本身就有clone方法,还用搞得这么麻烦吗?
虽然Java里面有clone方法,上面这么做还是很有意义的,可以更好地、更完整地体会原型设计模式。当然,后面会讲述如何使用Java里面的clone方法来实现克隆。
模式讲解
原型模式的功能
原型模式的功能实际上包含两个方面:
-
一个是通过克隆来创建新的对象实例;
-
另一个是为克隆出来的新的对象实例复制原型实例属性的值。
原型模式要实现的主要功能就是:通过克隆来创建新的对象实例。一般来讲,新创建出来的实例的数据是和原型实例一样的。但是具体如何实现克隆,需要由程序自行实现,原型模式并没有统一的要求和实现算法。
原型与new
原型模式从某种意义上说,就像是new操作,在前面的例子实现中,克隆方法就是使用new来实现的。但请注意,只是“类似于new”而不是“就是new”。
克隆方法和new操作最明显的不同就在于:new一个对象实例,一般属性是没有值的,或者是只有默认值;如果是克隆得到的一个实例,通常属性是有值的,属性的值就是原型对象实例在克隆的时候,原型对象实例的属性的值。
原型实例和克隆的实例
原型实例和克隆出来的实例,本质上是不同的实例,克隆完成后,它们之间是没有关联的,如果克隆完成后,克隆出来的实例的属性值发生了改变,是不会影响到原型实例的。