什么是原型模式

在介绍其他的设计模式时,为了方便理解,或多或少都会通过一些例子引入,便于理解。但在原型模式时,我并不打算通过例子开篇,因为原型模式足够简单,不需要通过例子来帮助理解。现在,我们直接进入正题。

原型模式的定义

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

解析定义

原型模式译自“Prototype”一词,该词的中文释义有:原型、样机、样板的意义。即便咱们对“原型”一词感到不知所云,但从其他的释义中也能猜出这个模式的核心就是“复刻”,而“复刻”又依赖于“已有的模板”。
从定义中能看出,该模式的目的在于 利用现有的原型创建新的对象 ,创建的方式为 拷贝 所要创建对象的类型由原型实例指定(用原型实例指定创建对象的种类)。

类图分析

创建型 - 原型模式(Prototype) - 图1
类图中,分为两种角色:

  • Prototype:声明一个可以克隆自身的接口;
  • ConcretePrototype:实现克隆自身的操作;

对于 Prototype,可使用接口,也可使用抽象类,可根据实际情况而定,在整个类图中其承担的作用也只是为了约束实现类。

正确的实现拷贝操作

在《设计模式-可复用面向对象软件基础》一书中指出,原型模式最困难的部分在于如何正确的实现拷贝操作。尤其是当对象结构包含循环引用时,这尤其棘手。
在 Java 中,提供了拷贝对象的支持,例如克隆。在 Object 类中,定义了一个默认实现 浅拷贝 的 clone() 方法,如果需要实现 深拷贝 ,则需要重写 clone() 方法。除此之外,想要克隆对象,类必须实现 Cloneable 接口

如果我们想要使用对象的 clone() 方法(不管是继承自 Object 类,还是重写了该方法),就必须让该类实现 Cloneable 接口,否则在调用 clone() 方法时,会得到一个 CloneNotSupportedException,但是在编译期并不会得到任何异常。这通常被认为是 Java 的一个糟糕设计,感兴趣的朋友可自行了解。

深拷贝与浅拷贝

到这里为止,如果对于深拷贝与浅拷贝的区别不清楚的可自行百度,这不是本节的重点。简而言之:

  • 对于基本数据类型属性:直接复制其值,对于这些属性的值原对象和拷贝对象各自变化,互不影响;
  • 对于引用类型的属性:深拷贝是递归处理,直到遇到基本数据类型。浅拷贝则是直接复制地址引用,因为浅拷贝是复制的地址引用,所以当修改其中一个对象时,另一个对象也会跟着改变(因为他们指向的是同一个对象)。
  • 有一些特殊的对象,遵循基本数据类型的原则,比如包装类,借助于不可变机制,能保证原对象和拷贝对象各自隔离。

对于原型模式来说,并没有规定必须使用浅拷贝实现或者深拷贝实现,所以在实现时,应根据实际需求出发,灵活调整。在满足需要的前提下,可优先考虑浅拷贝实现,因为浅拷贝实现更简单。

浅拷贝实现示例

Object 类的 clone() 方法默认就是浅拷贝的实现,所以,我们可以直接借助于 Object 类的 clone() 方法实现,示例代码如下:

  1. public class ShallowClone implements Cloneable {
  2. private int property1;
  3. private String property2;
  4. public void setProperty1(int property1) {
  5. this.property1 = property1;
  6. }
  7. public void setProperty2(String property2) {
  8. this.property2 = property2;
  9. }
  10. @Override
  11. protected Object clone() throws CloneNotSupportedException {
  12. return super.clone();
  13. }
  14. }

深拷贝实现示例

深拷贝实现方式有很多,这里介绍一种利用 jdk 序列化的方式进行深拷贝。原理是先将原对象进行序列化,再讲序列化得到的结果反序列化成为对象,反序列化后对象与原对象互相隔离。

  1. public class DeepClone implements Cloneable, Serializable {
  2. public String val1 = "test";
  3. public Object val2;
  4. @Override
  5. protected Object clone() throws CloneNotSupportedException {
  6. DeepClone target = null;
  7. try {
  8. ByteArrayOutputStream bos = new ByteArrayOutputStream();
  9. ObjectOutputStream oos = new ObjectOutputStream(bos);
  10. oos.writeObject(this);
  11. ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
  12. ObjectInputStream ois = new ObjectInputStream(bis);
  13. target = (DeepClone) ois.readObject();
  14. } catch (IOException | ClassNotFoundException e) {
  15. e.printStackTrace();
  16. }
  17. return target;
  18. }
  19. }

利用序列化实现深拷贝较为简单,但同时也要求该对象必须实现 Serializable 接口,且其中的引用类型的属性也必须实现 Serializable 接口。

  1. 也可以通过其他第三方工具实现序列化,比如 Jackson 、Gson 等。相比 jdk 实现的序列化,这些工具不要求对象必须实现 Serializable 接口,较为灵活。
  2. 除此之外,还可以通过在 clone() 方法中逐个处理需要深拷贝的属性的方式实现深拷贝,但该方法较为繁琐,需要挨个属性进行考虑,当该属性的类中包含有其他引用类型属性时,就需要递归处理。

代码实现

需求概述

现有一家中间商,该中间商经营的业务范围包括有房屋买卖,房屋租售等。当中间商促成买方和卖方进行交易时,会从现有的模板库中调出来标准的房屋买卖合同进行复印,然后由双方进行签字,合同生效;房屋租售业务流程类似。

根据这样的业务约定,我们可以套用原型模式进行设计,示例代码如下。

Prototype

  1. public abstract class Contract implements Cloneable {
  2. private String type; // 合同类型
  3. private String buyer; // 买方
  4. private String mediator = "不靠谱中间商"; // 中间商
  5. private String seller; // 卖方
  6. public Contract(String type) {
  7. this.type = type;
  8. }
  9. public void setBuyer(String buyer) {
  10. this.buyer = buyer;
  11. }
  12. public void setSeller(String seller) {
  13. this.seller = seller;
  14. }
  15. /**
  16. * 签署合同
  17. * @param productOwner 商品归属方
  18. * @param other 另一方
  19. */
  20. public abstract void signed (String productOwner, String other);
  21. @Override
  22. public Contract clone() {
  23. try {
  24. Contract clone = (Contract) super.clone();
  25. System.out.println(" 复印了一份房屋" + this.type);
  26. // TODO: copy mutable state here, so the clone can't change the internals of the original
  27. return clone;
  28. } catch (CloneNotSupportedException e) {
  29. throw new AssertionError();
  30. }
  31. }
  32. }

ConcretePrototype

  1. public class SalesContract extends Contract {
  2. public SalesContract() {
  3. super("买卖合同");
  4. }
  5. @Override
  6. public void signed(String productOwner, String other) {
  7. this.setBuyer(other);
  8. this.setSeller(productOwner);
  9. System.out.println(" 房屋买方为:" + other);
  10. System.out.println(" 房屋出售方为:" + productOwner);
  11. }
  12. }
  1. public class LeaseContract extends Contract {
  2. public LeaseContract() {
  3. super("租售合同");
  4. }
  5. @Override
  6. public void signed(String productOwner, String other) {
  7. this.setBuyer(other);
  8. this.setSeller(productOwner);
  9. System.out.println(" 房屋租方为:" + other);
  10. System.out.println(" 房屋出租方为:" + productOwner);
  11. }
  12. }

测试代码

  1. public class Client {
  2. public static void main(String[] args) throws CloneNotSupportedException {
  3. // 实例化原型购房合同
  4. Contract sales = new SalesContract();
  5. // 实例化原型租售合同
  6. Contract lease = new LeaseContract();
  7. System.out.println("|==> 现有[张三]欲购置[李四]的房子-------------------------------------|");
  8. Contract clone4Sales = sales.clone();
  9. clone4Sales.signed("李四", "张三");
  10. System.out.println("|==> 现有[杰克]欲租赁[汤姆]的房子-------------------------------------|");
  11. Contract clone4Lease = lease.clone();
  12. clone4Lease.signed("汤姆", "杰克");
  13. }
  14. }
  1. |==> 现有[张三]欲购置[李四]的房子-------------------------------------|
  2. 复印了一份房屋买卖合同
  3. 房屋买方为:张三
  4. 房屋出售方为:李四
  5. |==> 现有[杰克]欲租赁[汤姆]的房子-------------------------------------|
  6. 复印了一份房屋租售合同
  7. 房屋租方为:杰克
  8. 房屋出租方为:汤姆

扩展内容

在开发中,我们常常会给原型模式加一个原型管理器组件,该管理器组件内部维护所有原型的集合,负责初始化所有原型,并且在需要使用新对象的地方调用封装的方法直接获取拷贝对象。如下所示:

  1. public class ContractManager {
  2. private static final Map<String, Contract> CACHE = new HashMap<>();
  3. public static void loadCache() {
  4. Contract sales = new SalesContract();
  5. CACHE.put("sales", sales);
  6. Contract lease = new LeaseContract();
  7. CACHE.put("lease", lease);
  8. }
  9. public static Contract newInstance(String key){
  10. Contract contract = CACHE.get(key);
  11. if (contract == null) {
  12. throw new RuntimeException("不支持的合同类型");
  13. }
  14. return contract.clone();
  15. }
  16. }

源码中的应用

在 jdk 的源码中,很多类都实现了 Cloneable 接口,比如 ArrayList :

  1. public class ArrayList<E> extends AbstractList<E>
  2. implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
  3. public Object clone() {
  4. try {
  5. ArrayList<?> v = (ArrayList<?>) super.clone();
  6. v.elementData = Arrays.copyOf(elementData, size);
  7. v.modCount = 0;
  8. return v;
  9. } catch (CloneNotSupportedException e) {
  10. // this shouldn't happen, since we are Cloneable
  11. throw new InternalError(e);
  12. }
  13. }
  14. }