尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

原型模式:高效克隆对象的创建型设计模式详解

原型模式:高效克隆对象的创建型设计模式详解 1. 原型模式到底解决什么问题别和工厂模式搞混了如果你正在写一个系统需要创建大量相似但又不完全相同的对象比如游戏里成千上万个属性略有差异的怪物或者文档编辑器里复制一个包含复杂格式的图形你会怎么写最直接的想法可能是new一个对象然后挨个设置属性。但这样做有两个大问题一是性能差每次都要走一遍完整的构造和初始化流程二是代码耦合度高客户端必须知道对象内部的所有构造细节。原型模式Prototype Pattern就是为了解决这个“高效创建相似对象”的问题。它的核心思想非常简单通过复制一个现有实例原型来创建新对象而不是通过new关键字调用构造函数。这就像生物学的“克隆”你有一个细胞通过分裂就能得到一个一模一样的新细胞。很多人容易把它和工厂模式搞混。简单区分一下工厂模式关注的是“创建哪种产品”。你告诉工厂“我要一把斧头”工厂给你生产一把新的斧头。重点是隐藏具体产品的构造逻辑。原型模式关注的是“如何高效地复制一个产品”。你手头已经有一把精心打造、附了魔的传奇斧头现在你需要一百把一模一样的去武装你的士兵。重点是“复制”这个动作本身的高效和便捷。所以原型模式最关键的场景就两个1. 创建成本高比如对象初始化需要从数据库、网络加载大量数据2. 系统需要动态指定创建的对象类型运行时才知道要创建哪个具体类的对象。在 Spring 框架中Bean 的作用域为prototype时每次获取都会返回一个新的实例其底层思想就与原型模式一脉相承虽然 Spring 的实现更复杂。2. 理解原型模式的结构接口、实现与深浅拷贝原型模式的结构非常清晰通常包含以下角色原型接口 (Prototype Interface)声明一个克隆自身的方法通常是clone()或copy()。这是所有具体原型类的契约。具体原型类 (Concrete Prototype)实现原型接口提供克隆自身的具体实现。这是模式的核心。客户端 (Client)通过调用原型对象的克隆方法来创建新对象而无需知道其具体类。用代码来理解是最直接的。我们来看一个最简单的 Java 示例// 1. 原型接口 interface Prototype extends Cloneable { Prototype clone(); } // 2. 具体原型类 class ConcretePrototype implements Prototype { private String field; private ListString listField; public ConcretePrototype(String field, ListString listField) { this.field field; this.listField listField; } // 实现克隆方法 Override public Prototype clone() { try { // 这里调用的是Object的clone()是浅拷贝 ConcretePrototype copy (ConcretePrototype) super.clone(); // 对于引用类型如果需要深拷贝必须手动处理 // copy.listField new ArrayList(this.listField); return copy; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } // getters and setters... } // 3. 客户端使用 public class Client { public static void main(String[] args) { ConcretePrototype original new ConcretePrototype(原始数据, new ArrayList(Arrays.asList(A, B))); ConcretePrototype copy (ConcretePrototype) original.clone(); System.out.println(original copy); // false是两个不同的对象 System.out.println(original.getListField() copy.getListField()); // true浅拷贝共享同一个List引用 } }这段代码引出了原型模式实现中最关键、也最容易出错的概念浅拷贝 (Shallow Copy) 与深拷贝 (Deep Copy)。浅拷贝Object.clone()的默认行为。它复制对象的所有基本数据类型字段但对于对象引用字段只复制引用地址新旧对象共享同一个子对象。就像上例中的listField修改copy的列表original的列表也会跟着变。这在很多场景下是灾难性的。深拷贝不仅复制对象本身还递归复制其所有引用字段指向的对象生成一个完全独立的副本。要实现深拷贝通常需要在clone()方法中手动为新对象的每个引用字段创建新的实例。对于上面的ConcretePrototype一个简单的深拷贝实现如下Override public Prototype clone() { try { ConcretePrototype copy (ConcretePrototype) super.clone(); // 深拷贝关键为引用字段创建新的对象 copy.listField new ArrayList(this.listField); // 创建新的ArrayList复制元素 return copy; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } }现在original.getListField() copy.getListField()将返回false。在实际项目中如果原型对象内部结构非常复杂嵌套多层对象、集合等实现一个正确、高效的深拷贝可能非常棘手有时需要借助序列化/反序列化如 Java 的Serializable或第三方库如 Apache Commons Lang 的SerializationUtils.clone。3. 在不同语言中的实现与“坑点”原型模式的思想是通用的但不同编程语言对其的支持和实现方式差异很大。3.1 Java 的实现Java 通过Cloneable标记接口和Object.clone()本地方法提供了原生支持但设计上饱受诟病。Cloneable是个空接口它只是一个标记不包含任何方法。如果一个类没有实现Cloneable却调用了clone()会抛出CloneNotSupportedException。Object.clone()是 protected 方法你必须在子类中重写并提升为 public。默认是浅拷贝深拷贝需要开发者自己负责。不调用构造函数clone()直接复制内存块。这意味着如果对象构造过程中有副作用如注册监听器、初始化外部资源这些逻辑在克隆时不会执行。因此在 Java 中很多专家建议不要使用Cloneable/clone()而是实现一个自定义的拷贝构造函数或拷贝工厂方法这样更清晰、更可控。// 更推荐的拷贝构造函数方式 class ConcretePrototype { private String field; private ListString listField; public ConcretePrototype(ConcretePrototype other) { this.field other.field; this.listField new ArrayList(other.listField); // 深拷贝 } }3.2 C 的实现C 没有内置的克隆机制但可以通过拷贝构造函数和拷贝赋值运算符来实现原型模式。这也是实现深拷贝的标准场所。拷贝构造函数ClassName(const ClassName other)拷贝赋值运算符ClassName operator(const ClassName other)在 C 中你必须非常小心“三法则”如果定义了析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个通常都需要定义全部三个以及在 C11 之后的“五法则”加上移动构造函数和移动赋值运算符。忘记实现深拷贝会导致指针重复释放等严重内存错误。3.3 Python 的实现Python 中实现原型模式非常灵活主要使用copy模块。copy.copy(x)浅拷贝。创建一个新的复合对象然后尽可能将原始对象中找到的对象的引用插入其中。copy.deepcopy(x)深拷贝。创建一个新的复合对象然后递归地将原始对象中找到的对象的副本插入其中。你可以通过实现__copy__()和__deepcopy__()特殊方法来定制拷贝行为。import copy class Prototype: def __init__(self, value, list_value): self.value value self.list_value list_value def __copy__(self): # 自定义浅拷贝 new_one type(self)(self.value, self.list_value) # 注意这里list_value是引用传递 return new_one def __deepcopy__(self, memo): # 自定义深拷贝 import copy new_one type(self)(self.value, copy.deepcopy(self.list_value, memo)) return new_one # 使用 obj Prototype(1, [1, 2, 3]) shallow_copy copy.copy(obj) deep_copy copy.deepcopy(obj)3.4 JavaScript 的实现在 JavaScript 中对象复制是日常操作但同样存在深浅拷贝问题。浅拷贝Object.assign({}, obj)、扩展运算符{...obj}、Array.prototype.slice()。深拷贝JSON.parse(JSON.stringify(obj))有局限不能处理函数、undefined、循环引用等、structuredClone()现代浏览器和 Node.js 支持能处理更多类型和循环引用、使用 Lodash 的_.cloneDeep。对于需要原型模式的自定义类可以实现一个clone方法。class GameUnit { constructor(name, stats) { this.name name; this.stats { ...stats }; // 避免外部修改影响内部 } clone() { // 实现深拷贝 const clonedStats JSON.parse(JSON.stringify(this.stats)); return new GameUnit(this.name, clonedStats); } }一个通用坑点无论哪种语言当你的原型对象包含文件句柄、网络连接、数据库连接、线程等不可复制或唯一性的资源时克隆行为必须被慎重处理通常需要重新初始化或设置为null否则会导致资源冲突或状态混乱。4. 实战场景何时用怎么用如何管理原型理解了原理和实现我们来看看原型模式在哪些真实场景下能发挥价值。4.1 典型使用场景游戏开发这是教科书级的例子。一个“兽人步兵”原型对象定义了基础属性生命值、攻击力、模型路径。当需要生成一队兽人步兵时只需克隆这个原型然后微调每个实例的个别属性如位置、ID。这比每次从零构造一个兽人要高效得多。图形编辑器用户绘制了一个复杂的组合图形如一个流程图节点包含文字、边框、连接点。复制粘贴操作本质上就是克隆这个图形原型生成一个位置不同但外观和结构完全相同的新图形。配置对象系统有一个复杂的默认配置对象包含数百个参数。不同模块或用户可能需要基于这个默认配置进行少量修改。克隆默认配置作为起点比重新构建一个配置对象并逐一设置所有默认值要方便和安全。减少数据库查询某个对象初始化需要执行多次昂贵的数据库查询来填充数据。如果这个对象需要被多次使用且数据相对静态可以在第一次加载后将其缓存为原型后续需求通过克隆原型来获得对象避免重复查询。动态加载类在某些框架中你可以在运行时动态注册新的原型类。客户端只需要知道原型接口就可以通过一个标识符如字符串从原型管理器获取对应的原型并进行克隆无需在代码中硬编码具体的类名。这提高了系统的扩展性。4.2 原型管理器 (Prototype Manager)当系统中存在多种原型时直接让客户端记住并管理所有原型对象是不现实的。这时可以引入一个原型管理器或原型注册表它通常是一个简单的键值对集合如Map。public class PrototypeManager { private static MapString, Prototype prototypeMap new HashMap(); static { // 初始化时注册一些常用原型 prototypeMap.put(Soldier, new SoldierUnit(100, 10)); prototypeMap.put(Tank, new TankUnit(500, 50)); } public static void registerPrototype(String key, Prototype prototype) { prototypeMap.put(key, prototype); } public static Prototype getClone(String key) { Prototype prototype prototypeMap.get(key); if (prototype ! null) { return prototype.clone(); // 关键返回的是克隆体不是原型本身 } return null; } } // 客户端使用 Unit myArmy (Unit) PrototypeManager.getClone(Soldier);使用管理器时的一个关键细节getClone方法返回的一定是克隆体。如果你错误地返回了原型本身那么所有客户端拿到的都是同一个对象修改其中一个会影响到所有其他“克隆体”这完全违背了原型模式的初衷。4.3 与其它创建型模式的对比与选择在项目中选择设计模式时清晰地区分它们能帮你做出更好决策。模式核心目的适用场景工厂方法将对象创建过程延迟到子类解决“单个对象”的创建问题。创建逻辑复杂需要根据不同条件创建不同产品且产品有统一接口。抽象工厂创建一系列相关或依赖的对象族而不指定具体类。系统需要多个产品族且保证族内产品兼容。建造者将复杂对象的构建过程与表示分离允许逐步构造。对象构造参数多且有些参数可选希望构造过程清晰、灵活。原型通过复制现有实例来创建新对象避免昂贵的初始化开销。1. 创建成本高资源、时间。2. 系统需独立于其产品的创建、组合和表示。简单来说需要灵活控制创建过程或构建复杂对象- 考虑工厂或建造者。需要高效复制已有对象或运行时动态指定类型- 考虑原型。5. 在 Spring、Qt 等框架中的体现与自实现要点很多成熟框架都内置或体现了原型模式的思想理解它们有助于我们更好地使用框架。5.1 Spring Framework 中的原型作用域在 Spring 中当你将一个 Bean 的作用域定义为prototype时每次从容器中请求该 Bean通过getBean()或注入Spring 都会返回一个全新的实例。这可以看作是原型模式的一种应用Spring 容器缓存了 Bean 的定义原型每次根据这个定义“克隆”出一个新实例。需要注意的是Spring 的prototypeBean 的“克隆”过程实际上是重新执行了一遍 Bean 的初始化生命周期调用构造函数、注入依赖、执行初始化方法等并非 Java 默认的Object.clone()。这更接近于我们之前提到的“拷贝构造函数”方式。5.2 Qt/C 中的原型模式应用在 Qt 这样的 GUI 框架中原型模式常用于图形项QGraphicsItem的复制。Qt 的图形视图框架提供了QGraphicsItem::clone()虚函数具体的图形项子类如自定义的图表节点可以重写此函数来实现复制功能。当用户在界面上复制一个图形项时框架就是调用这个克隆方法。在《Qt C设计模式实战指南》这类资料中原型模式常被用于实现诸如“工具栏模板”、“图形元件库”等功能。你可以从元件库中拖拽一个“电阻”原型到电路图中实际上就是克隆了一个新的电阻实例。5.3 自己实现时的核心要点与检查清单如果你要在自己的项目中应用原型模式我建议按以下步骤和要点进行第一步评估是否真需要[ ] 创建对象的过程是否真的非常昂贵IO、计算、网络[ ] 是否需要创建大量相似对象[ ] 对象的类型是否需要运行时动态决定如果以上都不是直接用new或工厂可能更简单。第二步设计原型接口与类[ ] 定义一个清晰的克隆方法如Prototype clone()或Prototype copy()。[ ] 让具体原型类实现该接口。[ ]立即决定采用浅拷贝还是深拷贝。对于包含可变引用字段的类默认优先考虑深拷贝除非有明确的共享需求。第三步实现克隆方法Java考虑使用拷贝构造函数或静态工厂方法代替Cloneable。如果必须用clone()务必处理好所有引用字段。C正确实现拷贝构造函数和拷贝赋值运算符遵循三/五法则。Python实现__copy__和__deepcopy__。JavaScript实现clone()方法内部使用深拷贝逻辑如structuredClone或递归复制。[ ] 在clone方法中避免调用任何可能改变原型对象状态的操作。第四步处理复杂依赖与资源[ ] 如果原型对象持有数据库连接、文件句柄等资源在克隆时应该如何处理通常是置空或创建新连接[ ] 如果对象图存在循环引用你的深拷贝算法能否处理JSON.stringify不能需要递归算法加备忘录[ ] 考虑使用序列化/反序列化作为实现深拷贝的通用方案如 Java 的Serializable但要关注性能。第五步考虑引入管理器[ ] 如果原型种类多添加一个PrototypeManager来集中注册和获取。[ ] 确保管理器返回的是克隆体而不是原型本身。第六步测试[ ] 测试克隆出的对象与原型对象相等equals但不相同。[ ] 测试修改克隆对象的字段不会影响原型对象深拷贝测试。[ ] 测试包含集合、嵌套对象等复杂结构的克隆是否正确。[ ] 性能测试对比克隆和新建对象的开销验证使用模式的收益。最后记住原型模式是一种“以空间换时间”的模式。它通过预先创建并缓存原型对象牺牲了一些内存换来了运行时对象创建的速度。在对象创建成本高昂的场景下这笔交易非常划算。但在对象结构非常简单或者每个对象都差异巨大的场景下它可能反而增加了复杂度。
返回列表