什么是原型模式
在介绍其他的设计模式时,为了方便理解,或多或少都会通过一些例子引入,便于理解。但在原型模式时,我并不打算通过例子开篇,因为原型模式足够简单,不需要通过例子来帮助理解。现在,我们直接进入正题。
原型模式的定义
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
解析定义
原型模式译自“Prototype”一词,该词的中文释义有:原型、样机、样板的意义。即便咱们对“原型”一词感到不知所云,但从其他的释义中也能猜出这个模式的核心就是“复刻”,而“复刻”又依赖于“已有的模板”。
从定义中能看出,该模式的目的在于 利用现有的原型创建新的对象 ,创建的方式为 拷贝 ,所要创建对象的类型由原型实例指定(用原型实例指定创建对象的种类)。
类图分析
类图中,分为两种角色:
- Prototype:声明一个可以克隆自身的接口;
- ConcretePrototype:实现克隆自身的操作;
对于 Prototype,可使用接口,也可使用抽象类,可根据实际情况而定,在整个类图中其承担的作用也只是为了约束实现类。
正确的实现拷贝操作
在《设计模式-可复用面向对象软件基础》一书中指出,原型模式最困难的部分在于如何正确的实现拷贝操作。尤其是当对象结构包含循环引用时,这尤其棘手。
在 Java 中,提供了拷贝对象的支持,例如克隆。在 Object 类中,定义了一个默认实现 浅拷贝 的 clone() 方法,如果需要实现 深拷贝 ,则需要重写 clone() 方法。除此之外,想要克隆对象,类必须实现 Cloneable 接口。
如果我们想要使用对象的 clone() 方法(不管是继承自 Object 类,还是重写了该方法),就必须让该类实现 Cloneable 接口,否则在调用 clone() 方法时,会得到一个 CloneNotSupportedException,但是在编译期并不会得到任何异常。这通常被认为是 Java 的一个糟糕设计,感兴趣的朋友可自行了解。
深拷贝与浅拷贝
到这里为止,如果对于深拷贝与浅拷贝的区别不清楚的可自行百度,这不是本节的重点。简而言之:
- 对于基本数据类型属性:直接复制其值,对于这些属性的值原对象和拷贝对象各自变化,互不影响;
- 对于引用类型的属性:深拷贝是递归处理,直到遇到基本数据类型。浅拷贝则是直接复制地址引用,因为浅拷贝是复制的地址引用,所以当修改其中一个对象时,另一个对象也会跟着改变(因为他们指向的是同一个对象)。
- 有一些特殊的对象,遵循基本数据类型的原则,比如包装类,借助于不可变机制,能保证原对象和拷贝对象各自隔离。
对于原型模式来说,并没有规定必须使用浅拷贝实现或者深拷贝实现,所以在实现时,应根据实际需求出发,灵活调整。在满足需要的前提下,可优先考虑浅拷贝实现,因为浅拷贝实现更简单。
浅拷贝实现示例
Object 类的 clone() 方法默认就是浅拷贝的实现,所以,我们可以直接借助于 Object 类的 clone() 方法实现,示例代码如下:
public class ShallowClone implements Cloneable {
private int property1;
private String property2;
public void setProperty1(int property1) {
this.property1 = property1;
}
public void setProperty2(String property2) {
this.property2 = property2;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
深拷贝实现示例
深拷贝实现方式有很多,这里介绍一种利用 jdk 序列化的方式进行深拷贝。原理是先将原对象进行序列化,再讲序列化得到的结果反序列化成为对象,反序列化后对象与原对象互相隔离。
public class DeepClone implements Cloneable, Serializable {
public String val1 = "test";
public Object val2;
@Override
protected Object clone() throws CloneNotSupportedException {
DeepClone target = null;
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
target = (DeepClone) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return target;
}
}
利用序列化实现深拷贝较为简单,但同时也要求该对象必须实现 Serializable 接口,且其中的引用类型的属性也必须实现 Serializable 接口。
- 也可以通过其他第三方工具实现序列化,比如 Jackson 、Gson 等。相比 jdk 实现的序列化,这些工具不要求对象必须实现 Serializable 接口,较为灵活。
- 除此之外,还可以通过在 clone() 方法中逐个处理需要深拷贝的属性的方式实现深拷贝,但该方法较为繁琐,需要挨个属性进行考虑,当该属性的类中包含有其他引用类型属性时,就需要递归处理。
代码实现
需求概述
现有一家中间商,该中间商经营的业务范围包括有房屋买卖,房屋租售等。当中间商促成买方和卖方进行交易时,会从现有的模板库中调出来标准的房屋买卖合同进行复印,然后由双方进行签字,合同生效;房屋租售业务流程类似。
根据这样的业务约定,我们可以套用原型模式进行设计,示例代码如下。
Prototype
public abstract class Contract implements Cloneable {
private String type; // 合同类型
private String buyer; // 买方
private String mediator = "不靠谱中间商"; // 中间商
private String seller; // 卖方
public Contract(String type) {
this.type = type;
}
public void setBuyer(String buyer) {
this.buyer = buyer;
}
public void setSeller(String seller) {
this.seller = seller;
}
/**
* 签署合同
* @param productOwner 商品归属方
* @param other 另一方
*/
public abstract void signed (String productOwner, String other);
@Override
public Contract clone() {
try {
Contract clone = (Contract) super.clone();
System.out.println(" 复印了一份房屋" + this.type);
// TODO: copy mutable state here, so the clone can't change the internals of the original
return clone;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
ConcretePrototype
public class SalesContract extends Contract {
public SalesContract() {
super("买卖合同");
}
@Override
public void signed(String productOwner, String other) {
this.setBuyer(other);
this.setSeller(productOwner);
System.out.println(" 房屋买方为:" + other);
System.out.println(" 房屋出售方为:" + productOwner);
}
}
public class LeaseContract extends Contract {
public LeaseContract() {
super("租售合同");
}
@Override
public void signed(String productOwner, String other) {
this.setBuyer(other);
this.setSeller(productOwner);
System.out.println(" 房屋租方为:" + other);
System.out.println(" 房屋出租方为:" + productOwner);
}
}
测试代码
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
// 实例化原型购房合同
Contract sales = new SalesContract();
// 实例化原型租售合同
Contract lease = new LeaseContract();
System.out.println("|==> 现有[张三]欲购置[李四]的房子-------------------------------------|");
Contract clone4Sales = sales.clone();
clone4Sales.signed("李四", "张三");
System.out.println("|==> 现有[杰克]欲租赁[汤姆]的房子-------------------------------------|");
Contract clone4Lease = lease.clone();
clone4Lease.signed("汤姆", "杰克");
}
}
|==> 现有[张三]欲购置[李四]的房子-------------------------------------|
复印了一份房屋买卖合同
房屋买方为:张三
房屋出售方为:李四
|==> 现有[杰克]欲租赁[汤姆]的房子-------------------------------------|
复印了一份房屋租售合同
房屋租方为:杰克
房屋出租方为:汤姆
扩展内容
在开发中,我们常常会给原型模式加一个原型管理器组件,该管理器组件内部维护所有原型的集合,负责初始化所有原型,并且在需要使用新对象的地方调用封装的方法直接获取拷贝对象。如下所示:
public class ContractManager {
private static final Map<String, Contract> CACHE = new HashMap<>();
public static void loadCache() {
Contract sales = new SalesContract();
CACHE.put("sales", sales);
Contract lease = new LeaseContract();
CACHE.put("lease", lease);
}
public static Contract newInstance(String key){
Contract contract = CACHE.get(key);
if (contract == null) {
throw new RuntimeException("不支持的合同类型");
}
return contract.clone();
}
}
源码中的应用
在 jdk 的源码中,很多类都实现了 Cloneable 接口,比如 ArrayList :
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
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);
}
}
}