欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Scala 与设计模式(三):Prototype 原型模式

程序员文章站 2022-06-12 21:47:02
...

本文由 Prefert 发表在 ScalaCool 团队博客。

第一个生物是怎么诞生的? 从科学角度推测:是由第一个细胞从核糖核酸(RNA)不断的新陈代谢演变而来的。

第一个细胞其实是非常孤独的,但幸好它掌握了「分裂」与「分化」的本领,一定条件下可以一分为二,由此才能快速演变,出现现在的人类。

在开发过程中,我们也常有类似的场景,本文将以细胞分裂为例来介绍原型模式。

定义

「*」设计模式中提及的 原型模式 定义如下:

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

从定义中我们可以知道,原型模式中核心点就是 原型类拷贝

看到拷贝,有些同学脑中可能会浮现下面这张图:

可事实并没有这么简单。

Java 实现

回到开头的例子,假设细胞没有分裂能力,每个细胞产生的过程和时间是一样的,这无疑是费时的。

这也是「原型模式」第一个要解决的问题 — 通过拷贝加速效率

在 Java 中所有的 class 都继承自 java.lang.Object 类,Object 提供了一个 clone() 方法,通过它,就能实现对象的拷贝。

浅拷贝

我们利用 Cloneable 接口,来实现细胞的克隆:

public class Cell implements Cloneable {
    private String dna;
    private Organelle organelle; // 细胞器

    ... // 省略 get set 与 构造函数

    @Override
    public String toString() {
        return "Cell: {" +
                "DNA = " + dna + '\'' +
                "Organelle = " + organelle.toString() +
                '}';
    }

    @Override
    public Cell clone() {
        Cell cellCopy = null;
        try {
            cellCopy = (Cell) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return cellCopy;
    }
}

public class Organelle {
    private String cytoplasm; // 细胞质
    private String nucleus; // 细胞核

    ...// 省略get、set、toString() 与构造函数
}复制代码

以上我们便能调用 clone() 方法对复杂对象进行拷贝,以此来实现分裂的功能。

测试:

Cell cellA, cellB;

cellA = new Cell("AAAGTCTGAC", new Organelle("细胞质", "细胞核"));
System.out.println(cellA);

cellB = cellA.clone();
System.out.println(cellB);

System.out.println("cellA == cellB ? " + (cellA == cellB));
System.out.println("cellA-class == cellB-class? :" + (cellA.getClass() == cellB.getClass()));复制代码

看起来不错!但问题出现了:这里的 clone 只能拷贝到细胞本身信息,但不拷贝细胞的引用,不同细胞中包含的细胞器是一样的。

这其实是「浅拷贝」和「深拷贝」的问题。看看它们的区别:

  • 浅拷贝
    仅仅复制原有对象的值,而不复制它对其他对象的引用。

  • 深拷贝
    原有对象的值和引用都被复制。

验证:

System.out.println("cellA.Organelle == cellB.Organelle ? " + (cellA.getOrganelle() == cellB.getOrganelle()));复制代码

输出:

cellA.Organelle == cellB.Organelle ? true复制代码

可见,当前 clone() 方法执行的是浅拷贝,Java 中所有的对象都保存在全局共享的堆中。

只要能拿到某个对象的引用,引用者就可以随意修改对象,这显然是不好的。

接下来我为大家介绍一下深拷贝如何实现。

深拷贝

说到深拷贝,一般有两种实现方案:

1. 改变 clone 方法

既然问题出在细胞器(Organelle)的引用没有被复制,为其手动添加上即可。

首先修改引用类,使其支持 clone

public class Organelle implements Cloneable { 
  ... // 省略相同代码

  @Override
   protected Object clone() throws CloneNotSupportedException {
       Object object = null;
       try {
           object = super.clone();
       } catch (CloneNotSupportedException e) {
           e.printStackTrace();
       }
       return object;
   }复制代码

其次在 Cell 类的 clone() 方法中复制细胞器的引用:

    @Override
    public Cell clone() throws CloneNotSupportedException {
        Cell cellCopy = null;

        try {
            cellCopy = (Cell) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }

        if (cellCopy != null) {
            cellCopy.organelle = (Organelle) organelle.clone();
        }
        return cellCopy;
    }复制代码

测试结果:

cellA.organelle == cellB.organelle ? false复制代码

虽然功能是实现了,但是每个引用对象都要重写 clone(),太糟糕了!

2. 序列化对象

序列化是一个将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。

Cloneable 实现类似,需要序列化的类要求实现序列化接口。

public class Organelle implements Serializable { ... }
public class Cell implements Serializable {
  ... // 省略部分代码

  // 序列化实现深拷贝
  public Cell deepClone() throws CloneNotSupportedException, IOException, ClassNotFoundException {
    // 序列化(将对象写入流中)
    ByteArrayOutputStream bos=new  ByteArrayOutputStream();
    ObjectOutputStream oos=new  ObjectOutputStream(bos);
    oos.writeObject(this);

    // 反序列化(将对象从流中取出)
    ByteArrayInputStream bis=new  ByteArrayInputStream(bos.toByteArray());
    ObjectInputStream ois=new  ObjectInputStream(bis);
    return  (Cell)ois.readObject();
  }

}复制代码

注意:Cloneable 与 Serializable 接口都是 「marker Interface」,即它们只是标识接口,没有定义任何方法。

对比而言,序列化的实现方式不需要重写多个类的 clone() 方法,比第一种更加简便。

接下去看看 Scala 中如何实现原型模式。

Scala 实现

在 Scala 中,你用类似 Java 的方式来实现(Scala 提供了调用 Java 中 CloneableSerializable 的特质)

trait Cloneable extends java.lang.Cloneable

trait Serializable extends Any with java.io.Serializable复制代码

当然,Scala 中每个 case class 都拥有一个 copy() 方法,它会返回拷贝自原有实例的新实例,并且可以在拷贝的过程中改变一些值。

同样以细胞为例:

case class Cell(dna: String, organelle: Organelle)

case class Organelle(cytoplasm: String, nucleus: String)复制代码

测试一下:

val initialCell = Cell("AAAGTCTGAC", Organelle("细胞质", "细胞核"))
val cell1 = initialCell.copy()
val cell2 = initialCell.copy()
val cell3 = initialCell.copy(dna = "1234") // 可以在拷贝的时候重新赋值
System.out.println(s"cell1: ${cell1}")
System.out.println(s"cell2: ${cell2}")
System.out.println(s"cell3: ${cell3}")
System.out.println(s"cell1 and cell2 are equal: ${cell1 == cell2}")

// 输出
Cell 1: Cell(AAAGTCTGAC,Organelle(细胞质,细胞核))
Cell 2: Cell(AAAGTCTGAC,Organelle(细胞质,细胞核))
Cell 3: Cell(1234,Organelle(细胞质,细胞核))
cell1 and cell2 are equal: true复制代码

对比 Scala 和 Java 的实现代码,有没有发现 Scala 是如此的简洁。

诶? 为什么 cell1cell1 相等? 这会不会导致上面浅拷贝的问题呢?不存在的。

由于 case class 参数默认为 val,两个 case class 对象持有相同引用,但也不允许修改

总结

通过以上内容,我们对原型模式已有一些了解,一般来说原型模式中参与者有以下三类:

  • 抽象原型类:声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类、接口、甚至具体实现类(对应上面的 CloneableSerializable 接口)。
  • 具体原型类:实现抽象原型类声明的克隆方法,返回自己的一个克隆对象(Cell.class | Cell.class)。
  • 客户类:创建对象并克隆(Test.class)。

以下为 Java 与 Scala 中的实现方式对比:

拷贝方式 Java Scala
浅拷贝 具体原型类实现 Cloneable 具体原型类实现 Cloneable 或 具体原型类为 case class
深拷贝 具体原型类 + 引用类实现 CloneableSerializable 具体原型类 + 引用类实现 CloneableSerializable

当然原型模式通常还可以解决以下问题:

  • 创建新对象成本较大(如初始化需要占用较长的时间,占用太多的 CPU 资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
  • 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。

源码链接

如有错误和讲述不恰当的地方还请指出,不胜感激!