• 作者:老汪软件技巧
  • 发表时间:2024-08-23 11:02
  • 浏览量:

原型模式最早出现于 1963 年的一个叫 Sketchpad 的系统中,说起 Sketchpad 你可能并不熟悉,但是说起 CAD(计算机辅助设计),现在在工程设计领域几乎无人不知,其实 Sketchpad 就被认为是现代 CAD 程序的鼻祖,主要思想是拥有一张可以实例化成许多副本的原图,如果用户更改了主工程图,则所有实例也会更改。这便是原型模式最初的思维模型。

不过在面向对象编程中,对象的原型在变化时通常不会影响新实例对象。实际上,原型模式不仅在 Java、C++ 等主要基于类的编程语言中有广泛应用,而且还在一开始就是基于原型的 JavaScript 等编程语言中得到了发扬光大。原型模式的原理与使用其实很简单,但是要想用好它,我们除了要了解它的优点以外,更要重点注意它带来的问题以及掌握如何规避这些问题。下面,我们就一起来看看吧。

一、模式原理分析

原型模式的定义是:使用原型实例指定创建对象的种类,然后通过拷贝这些原型来创建新的对象。

你可以看到,定义中清晰地指出了两个关键点,一个是要建立原型,另一个是基于原型做拷贝。原理很简单,在现代编程中,我们经常会用到的 Ctrl+C 加 Ctrl+V 编程,可以说就是最直接的原型模式的实践之一。

我们来看看原型模式原始的 UML 图:

从这个 UML 图中,我们可以看到原型模式中的关键角色有三个:

使用者需要建立一个原型,才能基于原型拷贝出新实例。除此之外,使用者还需要决策什么时候使用原型、什么时候使用新实例,以及从原型到新实例之间的拷贝应该采用什么样的算法策略,这些都是使用者来进行控制和决定的。只不过通常我们会使用一个通用的抽象拷贝接口来对外提供拷贝。

那 UML 图对应的代码该怎么实现呢?可参考下面这段基于 Java Cloneable 接口的代码实现:

public interface PrototypeInterface extends Cloneable {
    PrototypeInterface clone() throws CloneNotSupportedException;
}
public class ProtypeA implements PrototypeInterface {
    @Override
    public ProtypeA clone() throws CloneNotSupportedException {
        System.out.println("Cloning new object: A");
        return (ProtypeA) super.clone();
    }
}
public class ProtypeB implements PrototypeInterface {
    @Override
    public ProtypeB clone() throws CloneNotSupportedException {
        System.out.println("Cloning new object: B");
        return (ProtypeB) super.clone();
    }
}
//ProtypeA以自己为原型通过拷贝创建一个新的对象newInstanceA
public static void main(String[] args) throws CloneNotSupportedException {
    ProtypeA source = new ProtypeA();
    System.out.println(source);
    ProtypeA newInstanceA = source.clone();
    System.out.println(newInstanceA);
}

代码中,我们定义的 PrototypeInterface 接口通过继承 Cloneable 接口并重写 clone() 方法来实现对象的拷贝,而 ProtypeA 和 ProtypeB 都可以在建立自己的原型对象后,调用 clone() 方法来创建新对象。这里要注意的是,Cloneable 接口本身是空方法,调用的 clone() 方法其实是 Object.clone() 方法。

从以上内容会发现,原型模式封装了如下变化:

所以说,原型模式从建立原型到拷贝原型生成新实例,都是对用户透明的,一旦中间任何一个小细节出现问题,可能获取的就是一个错误的对象。

原型模式拷贝构造函数_原型模式深拷贝_

二、使用场景分析

原型模式常见的使用场景有以下六种。

在实际的一些类库和组件中都有原型模式的应用。比如,Spring 中使用@Scope("prototype")注解来使得注入的 Java Bean 使用原型模式。Fastjson 中的 JSON.parseObject() 也是一种原型模式的实践。再比如,在 JDK 中,使用 cloneable 接口的都能实现原型模式。

原型模式的适用场景通常都是对已有的复杂对象或大型对象的创建上,在这样的场景中,创建对象通常是一件非常烦琐的事情,通过拷贝对象能快速地创建对象。

其实这里还涉及一个扩展知识点:浅拷贝与深拷贝。当我们在做对象拷贝时,需要在浅拷贝和深拷贝之间做取舍。如果类仅包含原始字段和不可变字段,可以使用浅拷贝;如果类还包含有可变字段的引用(比如,对象中包含对象),那么我们就应该使用深拷贝。

三、为什么要使用原型模式?

分析完原型模式的原理和使用场景后,我们再来说说使用原型模式的原因,主要有以下四个。

第一个,减少每次创建对象的资源消耗。 当类初始化消耗资源特别多时,原型模式特别有用。比如,在 AI 系统中,我们经常需要频繁使用大量不同分类的数据模型文件,在对这一类文件建立对象模型时,不仅会长时间占用 IO 读写资源,还会消耗大量 CPU 运算资源,如果频繁创建模型对象,就会很容易造成服务器 CPU 被打满而导致系统宕机。通过原型模式我们可以很容易地解决这个问题,当我们完成对象的第一次初始化后,新创建的对象便使用对象拷贝(在内存中进行二进制流的拷贝),虽然拷贝也会消耗一定资源,但是相比初始化的外部读写和运算来说,内存拷贝消耗会小很多,而且速度快很多。

第二个,降低对象创建的时间消耗。 比如,需要查询数据库来创建对象时,如果数据库正好繁忙或锁表中,那么这个创建过程就可能出现长时间等待的情况。在很多高并发场景中,稍微长时间的等待可能都是致命的,因为大量的数据和请求如洪水一般涌入服务器,很容易引起雪崩效应。这时使用原型模式就是相当于对对象创建的过程进行了一次缓存读取,而不必一直阻塞程序的执行。

第三个,快速复制对象运行时状态。原型模式相比于传统的使用 new 关键字创建对象还有一个巨大的优势,那就是当构造函数中包含大量属性或定制化业务逻辑时,不用完全了解创建过程也能快速创建对象。比如,当一个对象类有 30 个以上的属性或方法时(属性字段可能为另一个对象),如果你都通过 get 和 set 方法来创建对象,你会发现复制粘贴都是一件痛苦的事,因为你可能都忘记了哪些字段是必选、哪些又是有数据的。这也是我们在接收 HTTP 和 RPC 传输的 JSON 数据时,更愿意采用反序列化(也是一种原型模式的实践)到对象的方式,而不是 new 一个新对象再赋值的原因。

第四个,能保存原始对象的副本。 在某些需要保存历史状态的场景中,比如,聊天消息、上线发布流程、需要撤销操作的程序等,原型模式能快速地复制现有对象的状态并留存副本,方便快速地回滚到上一次保存或最初的状态,避免因网络延迟、误操作等原因而造成数据的不可恢复。

四、原型模式的优缺点

使用原型模式主要有以下三个大的优点。

当然,原型模式也不是十全十美的,它也有一些缺点。

原型模式可以说是创建型模式中灵活性最高的模式,不过在带来灵活性的同时,也带来了更大的风险,这对我们的设计与实现间接提出了更高的要求。