什么是原型模式
在介绍其他的设计模式时,为了方便理解,或多或少都会通过一些例子引入,便于理解。但在原型模式时,我并不打算通过例子开篇,因为原型模式足够简单,不需要通过例子来帮助理解。现在,我们直接进入正题。
原型模式的定义
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
解析定义
原型模式译自“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;}@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}}
深拷贝实现示例
深拷贝实现方式有很多,这里介绍一种利用 jdk 序列化的方式进行深拷贝。原理是先将原对象进行序列化,再讲序列化得到的结果反序列化成为对象,反序列化后对象与原对象互相隔离。
public class DeepClone implements Cloneable, Serializable {public String val1 = "test";public Object val2;@Overrideprotected 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);@Overridepublic 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 originalreturn clone;} catch (CloneNotSupportedException e) {throw new AssertionError();}}}
ConcretePrototype
public class SalesContract extends Contract {public SalesContract() {super("买卖合同");}@Overridepublic 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("租售合同");}@Overridepublic 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 Cloneablethrow new InternalError(e);}}}
