原型模式


设计一个类的时候,我们通常会使用到构造函数,这里类和对象的关系好比模具和构件的关系,对象总是从类中创建的。但是某些场景下是不允许类的调用者直接调用构造函数,也就说对象未必需要从类中衍生出来,现实生活中存在太多案例是通过直接 “克隆” 来产生新的对象,而且克隆出来的本体和克隆体看不出任何区别。

原型模式不单是一种设计模式,也是一种编程范型。简单理解原型模式 Prototype:不根据类来生成实例,而是根据实例生成新的实例。也就说,如果需要一个和某对象一模一样的对象,那么就可以使用原型模式。

定义

从设计模式的角度讲,原型模式是一种创建型模式,摆脱了类的构造模式,原型模式告诉我们,想要创建一个对象,我们不必关心对象的具体类型,而是找到一个对象,然后通过克隆来创建一个一模一样的对象。

原型模式的实现关键,是语言本身是否提供了 clone 方法。js 中提供了 Object.create 方法,可以方便的克隆对象。来看下 js 中如何实现 clone 操作的:

// Shape - 父类(superclass)
function Shape() {
  this.x = 0;
  this.y = 0;
}


// 父类的方法
Shape.prototype.move = function(x, y) {
  this.x += x;
  this.y += y;
  console.info('Shape moved.');
};


// Rectangle - 子类(subclass)
function Rectangle() {
  Shape.call(this); // call super constructor.
}


// 子类续承父类
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;


var rect = new Rectangle();
console.log(rect);  // 输出:Rectangle { x: 0, y: 0 }
rect.move(1, 1); // 输出:Shape moved.
console.log(rect); // 输出:Rectangle { x: 1, y: 1 }

这里我们使用 Object.create 实现了简单的克隆复用,java 中也有类似的操作方法实现:Cloneable 接口和 clone 方法。

Prototype 模式中实现起来最困难的地方就是内存复制操作,所幸在 Java 中提供了 clone () 方法替我们做了绝大部分事情。

大家都知道,所有的 java 类都继承自 java.lang.Object 类,而 Object 类默认提供了 clone 方法用来实现对象复制,能够实现的 java 类必须实现一个叫做 Cloneable 的接口,用来标识该类是可以被复制的,如果一个类没有实现 Cloneable 接口而调用 Object.clone 的话,那么 java 会抛出 CloneNotSupportedException 异常。

使用原型模式时,根据其成员是否也克隆,原型模式又分为:浅拷贝和深拷贝。

浅拷贝 Vs 深拷贝

首先来看一个例子,我们定义一个 Person 类,对它进行简单的测试:

public class Person implements Cloneable{

    private int age; // 定义年龄字段
    private Date birth; // 定义生日字段

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public Person(int age, Date birth) {
        this.age = age;
        this.birth = birth;
    }

    public Person() {
    }
}

测试类如下:

Date date = new Date();

Person p1 = new Person(23, date);
Person p2 = p1;
System.out.println(p1);
System.out.println(p2);

打印结果:

com.isoft.Person@1540e19d
com.isoft.Person@1540e19d

可以看出,两个对象地址都一模一样,那么这两个对象就是切切实实的一个对象,这种的话叫做 “复制引用”,使用图描述如下:

再来看看使用 clone 的情况下:

Date date = new Date();

Person p1 = new Person(23, date);
Person p2 = (Person) p1.clone();
System.out.println(p1);
System.out.println(p2);

输出如下:

com.isoft.Person@1540e19d
com.isoft.Person@677327b6

从结果看出,打印的对象已经是两个对象了,这种的话就叫做 “复制对象”,使用图简单描述如下:

我们继续,使用 clone 克隆的对象,其中 age 属于基础类型,而 Date 类型属于引用类型,基础类型数据直接复制时就是值的传递,没有任何问题,那么对于这种引用类型的数据使用 clone 后,到底如何呢?我们做一个测试:

Date date = new Date();

Person p1 = new Person(23, date);
Person p2 = (Person) p1.clone();
System.out.println(p1); // com.isoft.Person@1540e19d
System.out.println(p2); // com.isoft.Person@677327b6

System.out.println(p1.getAge() == p2.getAge()); // true
date.setTime(234234234L);
System.out.println(p1.getBirth()); // Sun Jan 04 01:03:54 CST 1970
System.out.println(p2.getBirth()); // Sun Jan 04 01:03:54 CST 1970
System.out.println(p1.getBirth() == p2.getBirth()); // true

可以看到,我们改变 date 对象导致 p1 和 p2 的 birth 都发生了变化,所以可以想象 p1 的 birth 和 p2 的 birth 实际指向的还是同一个 Date 对象,针对这种引用类型,对其拷贝一般有两种,一种是直接将原对象 birth 属性的引用值赋给新的对象 p2 的 birth 属性,这样两个对象的 birth 指向的是用一个 Date 对象,这种就叫做 “浅拷贝”;还有一种就是,将原对象 birth 指向的 Date 对象复制一份,创建一个相同的 Date 对象,然后将这个新的 Date 对象的引用赋给新拷贝的 p2 对象的 birth 属性,这样 p1 和 p2 的 birth 就分别指向了两个不同的 Date 对象,这种就叫做 “深拷贝”。

使用图简单描述如下,首先是浅拷贝:

然后是深拷贝的图示:

那么,如何实现深拷贝,简单的思路就是:通过 Object.clone 方法单独对某个引用属性进行拷贝,来看下代码实现,

我们修改 Person 类的 clone 方法,附上完整类代码:

public class Person implements Cloneable{

    private int age; // 定义年龄字段
    private Date birth; // 定义生日字段

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person p = (Person) super.clone();
        p.birth = (Date) birth.clone();
        return p;
    }

    public Person(int age, Date birth) {
        this.age = age;
        this.birth = birth;
    }

    public Person() {
    }

    // ...省略get、set
}

然后我们测试如下:

Date date = new Date();
Person p1 = new Person(23, date);
Person p2 = (Person) p1.clone();

System.out.println(p1 == p2);

date.setTime(234234234L);
System.out.println(p1.getBirth()); // Sun Jan 04 01:03:54 CST 1970
System.out.println(p2.getBirth()); // Wed Jun 26 19:43:15 CST 2019
System.out.println(p1.getBirth() == p2.getBirth()); // false

可以看到,我们同样地,修改 p1 对象的 birth 属性,但是 p2 中的 birth 并没有发生变化,这就是所谓的 “深拷贝”。

真的是深拷贝?

上面我们测试了,当我们重写 clone 后实现了引用类型的深拷贝,但是,试想一下,如果引用类型内部还存在引用型属性的话,那么拷贝后的对象是否实现了这种深层的拷贝呢?同样地,我们做一个测试:

我们重新定义下 Person 类,定义如下:

public class Person implements Cloneable{

    private int age; // 定义年龄字段
    private Date birth; // 定义生日字段

    private Address address;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person p = (Person) super.clone();
        p.birth = (Date) birth.clone();
        p.address = (Address) address.clone();
        return p;
    }

    public Person(int age, Date birth, Address address) {
        this.age = age;
        this.birth = birth;
        this.address = address;
    }

    public Person(int age, Date birth) {
        this.age = age;
        this.birth = birth;
    }

    public Person() {
    }

    // ...省略get、set
}

其中 Address、Code 类定义如下:

public class Address implements Cloneable{
    private Code code; // 地址的编号信息字段

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public Code getCode() {
        return code;
    }

    public void setCode(Code code) {
        this.code = code;
    }
}

...
public class Code {}

测试代码如下:

Date date = new Date();

Address address = new Address();
Person p1 = new Person(23, date, address);
Person p2 = (Person) p1.clone();

System.out.println(p1 == p2); // false

System.out.println(p1.getAddress()); // com.isoft.Address@1540e19d
System.out.println(p2.getAddress()); // com.isoft.Address@677327b6

System.out.println(p1.getAddress().getCode() == p2.getAddress().getCode()); // true

经过测试我们发下,虽然 p1 和 p2 的 address 引用的对象已经区分开来了,但是这两对象的 code 属性还是指向的同一个 Code 对象,使用图示说明如下:

所以,上面的这种复制其实并非真正意义上的 “深拷贝”,如果要实现 code 也是指向不同的对象该如何做呢?

受到前面的启发,我们要实现 “深拷贝” 就需要单独拷贝某个属性来实现,这样修改后的 Address、Code 类如下:

public class Address implements Cloneable{
    private Code code; // 地址的编号信息字段

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Address address = (Address) super.clone();
        address.code = (Code) code.clone();
        return address;
    }

    public Address(Code code) {
        this.code = code;
    }
    ...省略get、set
}

...
public class Code implements Cloneable{

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

同样地测试如下:

Date date = new Date();

Address address = new Address(new Code());
Person p1 = new Person(23, date, address);
Person p2 = (Person) p1.clone();

System.out.println(p1 == p2); // false

System.out.println(p1.getAddress()); // com.isoft.Address@1540e19d
System.out.println(p2.getAddress()); // com.isoft.Address@677327b6

System.out.println(p1.getAddress().getCode() == p2.getAddress().getCode()); // false

可以看到,p1 和 p2 的 address 对应的 code 属性已经是分别指向不同的对象了,图示如下:

序列化和反序列化实现深拷贝

首先来看下什么是序列化和反序列化:

序列化是指将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

从字节流创建对象的相反的过程称为反序列化。而创建的字节流是与平台无关的,在一个平台上序列化的对象可以在不同的平台上反序列化。

使用序列化和反序列化来实现深拷贝,我们还是以上面的代码进行示例:

首先需要为 Person、Address、Code 类实现 Serializable 接口,这里就不写了,测试类如下:

Date date = new Date();

Address address = new Address(new Code());
Person p1 = new Person(23, date, address);

// 首先将p1序列化存储
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(p1);

// 反序列化来实现p1的拷贝
byte[] bytes = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
Person p2 = (Person)ois.readObject();  //克隆好的对象

System.out.println(p1 == p2); // false
System.out.println(p1.getAddress() == p2.getAddress()); // false
System.out.println(p1.getAddress().getCode() == p2.getAddress().getCode()); // false

可以看出,使用序列化和反序列化可以很方便的直接实现深拷贝。

应用场景

原型模式一般很少单独出现,一般都是和工厂方法模式一起搭配使用,通过 clone 来创建新的对象,然后由工厂方法返回。依赖倒置原则提醒我们创建对象的时候尽量不要依赖具体的对象类型,原型模式就很好的印证了这句话,避免僵硬地使用 new 来进行对象创建。

优缺点

原型模式的优点:

  • 向客户隐藏新实例生成的细节
  • 某些环境下,复制对象比新建对象更有效
  • 提供让客户自主创建未知类型对象的方法
  • 减少子类的构造,原型模式通过克隆而不是工厂方法来产生一个对象
    原型模式的缺点如下:
  • 对象复制有时比较复杂,特别是对象层级嵌套很深时

总结

这节我们介绍了一种新的对象创建的模式,又分别介绍了深拷贝、浅拷贝的概念以及示例,各个语言中其实都会涉及到对象的深浅拷贝问题,实现思路也都不尽相同。又讲解了序列化和反序列化在深拷贝中的应用。


Author: Re:0
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Re:0 !
 Previous
适配器模式 适配器模式
定义适配器,其实很好理解,生活中也随处可见,比如电源适配器、usb 适配器等等,那么适配器模式,也被称为Wrapper 模式。
2022-03-08
Next 
建造者模式 建造者模式
定义所谓万丈高楼平地起,但是我们建造(Build)高楼时,需要经历很多阶段,比如打地基、搭框架、浇筑水泥、封顶等,这些都是很难一气呵成的。所以一般我们是先建造组成高楼的各个部分,然后将其一个个地组装起来,好比搭积木一般,分阶段拼接后组装成一
2022-03-07
  TOC