原型模式


原型模式

原型模式是一种特殊的创建型模式,它通过复制一个已有的对象来获取更多相同或者相似的对象。原型模式可以提高相同类型对象的创建效率,简化创建过程。

原型模式概述

西游记中孙悟空拔下几根身上的猴毛,轻轻一吹,可以复制(克隆)出很多和自己一摸一样的猴子,在面向对象软件设计领域被称为原型模式,孙悟空就是原型对象。在面向对象系统中可以通过复制一个原型对象得到多个与原型对象一模一样的新对象,这就是原型模式的动机。

定义:使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。

原型模式是一种对象创建型模式,它的工作原理很简单:将一个原型对象传给要发动创建的对象(即客户端对象),这个要发动创建的对象通过请求原型对象复制自己来实现创建过程。

原型模式是一种“另类”的创建型模式,创建新对象(也称为克隆对象)的工厂就是原型类自身,工厂方法由负责复制原型对象的克隆方法来实现。

通过克隆方法所创建的对象是全新的对象,在内存中拥有新的地址,对克隆所产生的对象进行任何修改都不会对原型对象造成影响,每一个克隆对象都是相互独立的。

原型模式结构与实现

原型模式结构

由图可知,原型模式包含以下3个角色:

  • Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,它可以是抽象类也可以是接口,甚至还可以是具体实现类。
  • ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
  • Client(客户类):在客户类中,让一个原型对象克隆自身从而创建一个新的对象,只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。

浅克隆与深克隆

根据在复制原型对象的同时是否复制包含在原型对象中引用类型的成员变量,原型模式的克隆机制分为两种,即浅克隆(Shallow Clone)和深克隆(Deep Clone)。

  1. 浅克隆

    对于浅克隆而言,int、double、byte、boolean、char等基本数据类型(值类型的成员变量)被复制到克隆对象,而类、接口、数组等复杂数据类型(引用类型的成员变量)并没有被复制到克隆对象,只是让克隆对象的引用类型的成员变量也指向相同的内存地址。

  2. 深克隆

    对于深克隆而言,与浅克隆相对,无论是值类型还是引用类型,所有成员变量都复制一份给克隆对象。

原型模式实现

实现原型模式的关键在于如何实现克隆方法,以下是两种在Java语言中常用的克隆实现方法。

  1. 通用实现方法

    通用的克隆实现方法是在具体原型类的克隆方法中实例化一个与自身类型相同的对象并将其返回,同时将相关的参数传入新创建的对象中,保证它们的成员变量相同。示意代码如下:

    public abstract class Prototype {
        public abstract Prototype clone();
    }
    
    public class ConcretePrototype extends Prototype {
        private String attr;//成员变量
        
        public void setAttr(String attr) {
            this.attr = attr;
        }
        
        public String getAttr() {
            return this.attr;
        }
        
        //克隆方法
        public Prototype clone() {
            Prototype prototype = new ConcretePrototype();//创建新对象
            prototype.setAttr(this.attr);
            return prototype
        }
    }
    

    在客户类中只需要创建一个ConcretePrototype对象作为原型对象,然后调用其clone()方法即可得到对应的克隆对象,例如:

    ...
    ConcretePrototype prototype = new ConcretePrototype();
    prototype.setAttr("Sunny");
    ConcretePrototype copy = (ConcretePrototype)prototype.clone();
    ...
    

    这是原型方法的通用实现,与编程语言本身的特性无关,其他面向对象编程语言也可以使用这种形式来实现对原型对象的克隆。

  2. Java语言中的clone()方法和Cloneable接口

    在java语言中,所有的Java类均继承自java.lang.Object类,Object类提供了一个clone()方法,可以将一个对象复制一份。因此在Java中可以直接使用Object提供的clone()方法实现对象的浅克隆。

    需要注意的是能够实现克隆的Java类必须实现一个标识接口Cloneable,表示这个Java类支持被复制。如果一个类没有实现这个接口但是调用了clone()方法,Java编译器将抛出一个CloneNotSupportedException异常。如下代码所示:

    public class ConcretePrototype implements Cloneable {
        ...
        public Prototype clone() {
        	Object object = null;
            try {
                object = super.clone();//浅克隆
            }
            catch (CloneNotSupportedException exception) {
                System.err.println("Not support cloneable");
            }
            return (Prototype)object;
        }
        ...
    }
    

    在客户端创建原型对象和克隆对象也很简单,代码如下:

    Prototype prototype = new ConcretePrototype();
    Prototype copy = prototype.clone();
    

不过在使用第二种方法方法时,实现的是浅克隆,如果原型对象中有引用类型的值就不适合使用该方法。不过在Java语言中可以通过序列化(Serialization)等方式实现深克隆。序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个复制,而原对象仍存在于内存中。通过序列化实现的复制不仅可以复制对象的本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流将其读取出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。

public class ConcretePrototype implements Serializable {
    ...
    //使用序列化技术实现深克隆
    public ConcretePrototype deepClone() throws IOException, ClassNotFoundException, OptionalDataException {
        //将对象写入流中
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bao);
        oos.writeObject(this);
        //将对象从流中取出
        ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return (ConcretePrototype)ois.readObject();
    }
    ...
}
try {
    copy = prototype.deepClone();
} catch(Exception e) {
    System.err.println("克隆失败!")
}

原型管理器

原型管理器(Prototype Manager)将多个原型对象存储在一个集合中供客户端使用,它是一个专门负责克隆对象的工厂,其中定义了一个集合用于存储原型对象,如果需要某个原型对象的一个克隆,可以通过复制集合中对应的原型对象来获得。在原型管理器中针对抽象原型类进行编程,以便扩展,结构如下图:

典型的原型管理器的实现代码片段如下:

import java.util.*;

public class PrototypeManager {
    private Hashtable prototypeTable = new Hashtable();//使用Hashtable存储原型对象
    public PrototypeManager() {
        prototypeTable.put("A", new ConcretePrototypeA());
        prototypeTable.put("B", new ConcretePrototypeB());
    }
    
    public void add(String key, Prototype prototype) {
        prototypeTable.put(key, prototype);
    }
    
    public Prototype get(String key) {
        Prototype clone = null;
        clone = ((Prototype)prototypeTable.get(key)).clone();//通过克隆方法创建新对象
        return clone;
    }
}

在实际开发中可以将PrototypeManager设计为单例类,确保系统中有且仅有一个PrototypeManager对象,这样既有利于节省系统资源,还可以更好地对原型管理器对象进行控制。

原型模式优/缺点与适用环境

原型模式作为一种快速创建大量相同或相似对象的方式,在软件开发中的应用较为广泛,很多软件提供的复制(Ctrl+C)和粘贴(Ctrl+V)操作就是原型模式的典型应用。

优点

  1. 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率;
  2. 扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少原型类对原有系统没有任何影响;
  3. 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式不需要,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品;
  4. 可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(例如恢复到某一历史状态),可辅助实现撤销操作。

缺点

  1. 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时需要修改源代码,违背了开闭原则;
  2. 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。

适用环境

  1. 创建新对象成本较大(例如初始化需要占用较长时间、占用太多的CPU资源或网络资源),新对象可以通过复制已有对象来获得,如果是相似对象,则可以对其成员变量稍作修改;
  2. 系统要保存对象的状态,而对象的状态变化较小;
  3. 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。