C++ 中的原型设计模式

2025 年 5 月 13 日 | 阅读 12 分钟

原型设计模式 是一种创建型设计模式,它允许通过复制现有对象“原型”而不是使用构造函数来创建新对象。当需要创建的对象消耗大量资源、耗时过长或需要复杂配置时,此模式最有价值。与通过应用新对象创建机制来构建对象不同,该模式利用原型对象来创建新对象的精确副本或略有修改的副本。

在以下情况下,原型模式很重要:创建对象时,类型选择取决于某些条件,并且对象是使用近似参数创建的。它使开发人员能够将某些对象重用为创建其他对象的模板,因为它有助于减少大量类型初始化的使用。

从根本上说,原型模式允许客户端代码独立于实际的对象创建类。这是通过使用绕过对象实际实现的复制机制来实现的。这种抽象不仅有助于使系统摆脱结构性依赖,而且遵循开放/封闭原则,使系统对添加/扩展开放,但对更改关闭。

这种特定的设计模式通常在速度至关重要时使用,因为对象的制造成本很低。例如,在图形应用程序中,创建具有预设参数的形状的复制克隆比从头开始绘制形状要有效得多。同样,在应用程序初始化需要数据库、文件或任何其他资源的情况下,原型模式可以节省持续初始化多个对象以获得更大效果的需要。

原型模式的第二个主要优点是它在开发具有细微差异的对象时的适用性。通过克隆对象返回的实例可以以不影响原型的方式进行更改,以防原始对象由开发人员自定义。这可以防止在复制原始对象时损坏实际原型的可能性,同时在过程中提供牢固的参考点。

原型模式的核心组件

原型设计模式围绕三个主要组件:第一个称为原型接口,第二个称为具体原型,最后一个是客户端代码。这些组件结合在一起,可以有效地进行对象克隆,而客户端不直接依赖于对象的具体类。

1. 原型接口

原型接口是模式的第一部分。它制定了一项合同,保证所有实现类都能生成自己的克隆。该接口通常包含一个 `clone` 方法,用于生成对象的副本。主要通过抽象克隆对象的过程,该接口使客户端代码免受实际克隆创建过程的影响。

克隆方法可以是浅复制或深复制,即简单克隆或完整克隆。浅复制仅复制对象生成中的字段,保留对其他对象的引用。这种方法速度更快,并且带有副作用,当复制的对象共享可变资源时,就会遇到这些副作用。然而,深复制会创建所有嵌套对象的副本;复制的对象与原始对象之间没有链接。选择进行浅复制还是深复制取决于应用程序的需求和原型类型。

2. 具体原型

具体原型定义了原型接口,但实际上包含克隆操作。它捕获客户端想要模仿的数据和行为。复制逻辑可以是特定于对象的,如果对象在数据结构或关联方面存在一些复杂性,那么具体原型类就负责如何复制它。

例如,在分层对象模型的上下文中,具体原型必须复制其子对象才能正确地重新创建复制的实例。这使得克隆相当复杂但更可行,因为它保留了原型架构和功能结构。

具体原型类更多地作为客户端的示例,以便除了依赖类之外,还可以为该类创建新对象。这使得系统易于模块化,从而易于维护和扩展。

3. 客户端代码

客户端代码与原型接口通信,以从原型的克隆中生成相似的对象。这样,客户端就不了解原型的具体实现,只能访问抽象接口。这种解耦不仅增加了灵活性,还使用户能够轻松处理不同类型的原型。

在典型的实现中,客户端管理一个原型注册表或集合,这与 Actor 模型中的实现略有不同。还有一个注册表充当预定义模板的集中存储库,每个模板对应一个特定的对象类型。事实上,当客户端需要一个新对象时,它会来到注册表,获取正确的原型并克隆它。随后,客户端根据自己的偏好修改复制的对象,原型保持不变。

更重要的是,原型模式基于克隆,克隆被呈现为对象创建的一个方面,这与其他的创建型模式不同。与主要通过继承具体类来提供新对象创建的工厂方法或抽象工厂模式不同,该模式主要通过对象克隆来处理对象创建。因此,它在生成具有复杂初始化逻辑或动态设置的对象时很有用。

使用原型模式的优点

总而言之,原型设计模式似乎提供了几个明显的优势,特别是在处理需要高效且不受特定对象类别影响的对象创建问题时。由于其避免从头开始克隆和专注于创建新对象的能力,该模式解决了软件开发中的各种问题。以下是使用原型模式的关键优势:

  1. 高效的对象创建
    • 原型模式的一个优点,如前所述,是可以快速创建任何类的对象。当构造对象需要大量资源利用时,例如初始化大型数据集、连接到数据库和加载外部资源,原型模式不需要重复完整的对象构造过程。相反,它通过复制现有对象来创建新对象,在大多数情况下,这个过程效率要高得多。
    • 这种效率在性能关键型系统、视频游戏、模拟、实时应用程序等领域最为有用,在这些领域,创建多个非平凡对象的实例速度必然很快。例如,在游戏中,系统可以复制一个原型,而不是开发具有相同属性/特性的应用程序特定对象(如敌人角色)的新实例。
  2. 简化的对象初始化
    • 当对象需要带有大量初始化逻辑时,会涉及复杂性,这时原型模式就很有用。
    • 原型模式尤其适用于需要开发具有复杂创建过程的新对象的情况。架构师不必将复杂的设置过程直接包含在对象的创建机制中,而是有机会将配置放入原型中。为了创建新实例,系统会创建一个原型的副本,以便其中包含的配置保持固定。这使得创建更容易,并减少了成员在工作中复制代码的问题。
    • 此外,它还增强了关注点分离的已知原则。初始化详细信息封装在原型中,客户端代码不包含复杂的对象创建代码。与所有内容纠缠在一起相比,它还使系统更具模块化,并且更容易调试和维护。
  3. 支持动态对象创建
    • 然而,在某些情况下,在编译时可能不知道需要创建的确切对象类型。原型模式的思想更具通用性,因为对象可以通过克隆在运行时创建。系统还可以记录一组原型,其中每个原型都反映了所讨论对象类型的相应类型。如果客户端需要一个新实例,它会从注册表中提取正确的原型,然后复制它。
    • 这种动态方法在这些场景中可能非常有价值,尤其是在需要可扩展系统的框架或库中。例如,可以有一个用于创建形状的图形应用程序,用户可以决定加载哪些形状。因此,他们可以注册自定义形状原型,而不是硬编码每种形状类型的创建过程,应用程序可以克隆它们。
  4. 独立于具体类
    • 通过独立于具体类来评估新对象的构造,原型模式遵循了抽象和模块化原则。客户端仅使用原型的接口,而不知道正在复制的对象的实际类。它还允许更自由地更改引入系统的原型类型,因为它们不会影响客户端代码。
    • 它还符合另一个基本的软件设计规则,即开放/封闭原则。系统的可扩展性可以通过引入新原型来完成,这些原型包含在此功能空间中,而无需触摸现有代码并引入新错误。

何时使用原型模式

  • 必须指出,虽然原型设计模式是一种非常有用的、可移植的创建型模式,但它并非总是可行的。它的适用性源于您可以在某些情况下创建对象,而在这些情况下,使用构造函数或工厂方法等各种方法是不可能或不具有生产力的。以下是原型模式是理想选择的关键情况:
  1. 当对象创建成本高昂时
    • 原型模式的最大实用性在于创建对象时,因为这可能是一个耗时的过程。例如,如果某个对象实例化涉及大量的计算工作、大型数据结构或与其他系统(如数据库或服务器)的连接,那么不断创建这些对象将变得非常昂贵。然而,原型模式可以实例化第一个实例,然后在复制它以生成其他对象之前对其进行修改。
    • 这种方法大大降低了开销,尤其是在需要频繁进行性能测试的场景中。例如,在 Maya 等 3D 图形应用程序中,从头开始创建汽车或建筑物等复杂对象的多个实例需要大量资源。克隆预配置的原型是有效且一致的。
  2. 当对象初始化很复杂时
    • 在对象可以初始化、满足先决条件或其依赖的结构准备就绪之前,可能需要几个步骤。当我们将此逻辑直接放入构造函数中时,它只会使代码混乱且难以管理。
    • 原型模式在此很有用,因为它将复杂的初始化代码隐藏在原型中。原型配置完成后,它就可以用作创建类新实例的模式。这种关注点分离使代码更清晰、更易于调试,同时确保每个克隆都源自相同的模板对象。
  3. 当您需要避免子类化时
    • 当子类化可能产生过多的类或过度简化应用程序的某些方面时,原型模式是一种优雅的解决方案。无需创建许多子类来描述不同类型的对象,可以生成一个原型,并可以对其进行即时复制和修改。
    • 这种方法对于整体系统可维护性更好,因为它最大限度地减少了对大型类层次结构的需求。例如,在绘图应用程序中,不必为每种形状(如圆形、正方形、三角形等)创建一个类,而是可以创建一个 Shape 原型,并复制它来分别创建圆形、正方形和三角形。
  4. 当系统需要动态对象创建时
    • 原型模式在需要创建的对象类在运行时才知的系统中特别有用。在这些情况下,原型注册表将使系统能够创建动态创建的对象。这些存储库允许在不硬编码对象创建逻辑的情况下调用原型进行克隆。
    • 例如,在基于插件的设计中,应用程序可能需要或可能必须实例化属于动态加载的插件的对象。原型模式通过允许每个插件注册一个系统可以实例化任意多次的原型来提供帮助。
  5. 当对象具有分层结构时
    • 后者反过来是具有分层结构或由嵌套组件组成的对象,它们将最有利于使用原型模式。克隆这些对象时,会复制所有关系,并保留复合元素的结构。这种功能在数据可视化、场景图或组织结构图等领域尤其有用。
    • 例如,克隆操作,如在树形数据结构的根节点的情况下,可以使用原型来克隆子节点。这使得复制其他复杂性(如它们的层次结构)更容易,同时避免了这些层次结构的失真。
  6. 当您需要创建许多相似对象时
    • 使用原型模式的明显场景是需要创建大量相似对象时。与直接构造每个对象不同,系统可以创建一个新对象实例的副本,该副本可以设置为模板或角色模型,以便对象具有相同的属性。
    • 例如,在文档处理系统中,经常或定期生成的文档可以预先存储为发票、报告、信件等原型。在需要新文档的情况下,系统会复制必要的模板,然后进行某些修改,例如输入收件人信息或插入文本。

编码

输出

 
Circle with radius: 5
Rectangle with width: 3, height: 4
Modified Shapes:
Circle with radius: 10
Rectangle with width: 6, height: 8   

结论

总之,原型设计模式 是一种丰富的创建型设计模式,它利用原型来实例化完全初始化的对象。它最适用于对象创建计算成本高昂且需要在运行时进行复杂设置或配置的情况。应用该模式后,对象性能会提高,代码会从冗余中解放出来,并确保不同的实例表现相似,因为它们是现有对象的克隆,而不是新的构造。

原型模式的第二个优点是上述方法固有的易于自定义。这意味着克隆的对象是唯一的,并且可以在不影响原型的情况下进行自定义;这使得克隆非常适合开发基础 对象 的版本。原型注册表的特异性带来了更多的灵活性——可以在运行时生成对象,并且系统是可变的。

该模式也很好地融入了模块化和抽象原则,有助于提高代码质量,并符合开放/封闭原则。总的来说,原型模式在图形应用程序、模拟和分层 数据结构 等高影响复杂问题中表现出色,但不适用于所有情况,特别是低成本创建活动,如简单的对象生成。

总而言之,原型模式是一个强大的工具,它使系统易于构建,能够很好地扩展,易于维护,并且能够优化对象创建,同时承担最少的开销。