设计模式(六):原型模式——当“复制一个现成对象”比“重新造一个”更划算

举报
小不点和猫 发表于 2026/11/11 09:01:55 2026/11/11
【摘要】 一、模式原理定义:原型模式用原型实例指定要创建的对象的种类,并通过复制这个原型来创建新对象,而不是通过 new 调用构造函数。拆开看,这个定义包含三个要点:以实例为模板:被复制的对象本身充当“创建说明书”,使用方不需要知道产品的具体类名;绕开构造函数:复制不走 new,因此不重复执行昂贵的初始化逻辑;复制的是状态:新对象从原型继承创建时刻的状态,而不是从零开始。意图:当对象的创建过程昂贵(...

一、模式原理

定义:原型模式用原型实例指定要创建的对象的种类,并通过复制这个原型来创建新对象,而不是通过 new 调用构造函数。

拆开看,这个定义包含三个要点:

  • 以实例为模板:被复制的对象本身充当“创建说明书”,使用方不需要知道产品的具体类名;
  • 绕开构造函数:复制不走 new,因此不重复执行昂贵的初始化逻辑;
  • 复制的是状态:新对象从原型继承创建时刻的状态,而不是从零开始。

意图:当对象的创建过程昂贵(读库、读文件、复杂计算、远程调用)或复杂(构造参数需要从现有对象推导),而系统中又已经存在一个状态可用的实例时,通过复制它来获得新对象,把“创建”从“构造”变成“克隆”。

核心思想:把“造什么、怎么造”的知识从类转移到实例上——原型本身就是模板,复制就是创建。

GoF 原版结构中,参与者只有两个:

  • Prototype:声明 clone() 方法(实现 Cloneable 接口或等价机制);
  • ConcretePrototype:实现复制自身的方法,返回一个与自己状态相同的新对象。

使用方持有某个原型引用,调用 clone() 得到新对象——它不需要 new,也不需要知道具体类。这带来一个独特的收益:可以在运行期动态替换原型,从而改变“接下来创建出来的对象是什么”,而不改任何代码。

代价一句话:你换来的是“跳过昂贵初始化”和“不依赖具体类名”,付出的是深拷贝/浅拷贝的正确性负担,以及复制逻辑散落在每个类里、难以统一管理。

二、先看一个真实痛点

报表服务需要为每个租户生成一份月度报表对象。报表对象的初始化很重:从数据仓库拉取聚合数据、加载该租户的模板配置、预计算图表数据、建立缓存索引——单次构造约 1.2 秒。

问题出在“生成”环节:用户点一次“生成”,实际要产出多个变体——PDF 版、Excel 版、邮件摘要版。三个变体共享 90% 的数据(租户信息、聚合结果、模板配置),只有导出格式和少量字段不同。如果每个都 new Report(tenantId, month),就是三次 1.2 秒的重复劳动,而且三次拉取的数据可能因为期间数据更新而不一致。

有人试过把构造好的对象直接传给三个导出器,结果更糟:PDF 导出器在对象上设置了一个 format 字段,Excel 导出器读到了被改过的状态——它们共享了同一个可变对象。

这就是原型模式回应的压力:创建一个“和现有对象几乎一样、只有少量差异”的新对象时,重新构造既昂贵又可能不一致,直接共享引用又会互相污染。正确做法是复制一份,但复制必须知道“哪些字段该深拷贝、哪些可以共享”。

三、本质与结构速写

本质:让对象自己负责“复制自己”,使用方通过克隆而非构造获得新实例。

原型模式真正的难点不在“怎么调 clone()”,而在复制语义。这里必须分清三组概念:

浅拷贝:复制对象本身和它的基本类型字段,但引用类型字段仍指向同一对象。复制出来的两个对象共享内部的列表、Map、子对象。

深拷贝:递归复制所有引用类型字段,新对象与原型完全独立。

混合拷贝:部分字段深拷贝、部分共享——比如不可变字段(字符串、配置快照)可以共享,可变集合必须深拷贝。这是实践中真正需要的语义,也是两种语言都没有开箱即用支持的地方。

三条不变量,审查时必须检查:

  • 复制后的对象与原对象在可变状态上互不影响(除非业务明确要求共享);
  • 复制不重新执行昂贵初始化(否则原型模式失去意义);
  • 原型本身的状态在复制时是稳定且合法的(不能复制一个正在被修改的对象)。

四、Java 实现:Cloneable 是个陷阱,要看清它的边界

4.1 标准形态与它的三个坑

public class Report implements Cloneable {
    private String tenantId;
    private Month month;
    private List<ChartData> charts;       // 可变引用
    private TemplateConfig template;      // 可变引用

    @Override
    public Report clone() {
        try {
            Report copy = (Report) super.clone();   // 浅拷贝:charts/template 仍共享
            copy.charts = new ArrayList<>(this.charts);   // 手动深拷贝可变集合
            copy.template = this.template.copy();          // 若 template 不可变,可省略
            return copy;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }
}

使用方:

Report base = reportService.build(tenantId, month);  // 昂贵,只做一次
Report pdf   = base.clone(); pdf.setFormat(PDF);
Report excel = base.clone(); excel.setFormat(EXCEL);

三个必须点明的坑:

坑一:super.clone() 是浅拷贝,且不调用构造函数。 它按内存逐字段复制,charts 和 template 两个引用会被原样复制——两个 Report 指向同一个 List。不手动处理,就是前面那个“导出器互相污染”的事故。同时,因为不调用构造函数,任何在构造函数里做的初始化、注册、校验都不会执行——如果类依赖构造函数建立不变量,clone() 可能产出一个“绕过校验”的对象。

坑二:Cloneable 是一个标记接口,但它不声明 clone()。 clone() 来自 Object,且是 protected 的。一个类不实现 Cloneable 却调用 super.clone(),会抛 CloneNotSupportedException。这个设计被普遍认为是 Java 语言的历史失误——接口没有约定行为,行为在 Object 上,且是受检异常。

坑三:继承链上的复制会悄悄变浅。 如果 Report 有子类 DetailedReport 且子类新增了可变字段,父类的 clone() 不会处理它——子类必须重写。忘记重写,就会得到一个“看起来是子类、但子类字段共享”的对象。这是原型模式在 Java 里最难排查的一类 bug。

4.2 更可控的替代:拷贝构造函数

public class Report {
    private final String tenantId;
    private final Month month;
    private final List<ChartData> charts;
    private final TemplateConfig template;

    // 拷贝构造函数:显式、可读、不依赖 Cloneable
    public Report(Report other) {
        this.tenantId = other.tenantId;
        this.month    = other.month;
        this.charts   = new ArrayList<>(other.charts);      // 深拷贝可变集合
        this.template = other.template;                     // 不可变则共享
    }
}

拷贝构造函数绕开了 Cloneable 的全部坑:调用构造函数意味着不变量会被重新建立、子类必须显式调用父类拷贝构造(编译器强制)、异常语义清晰。在 Java 里,拷贝构造函数通常比 clone() 更值得推荐——除非需要在运行期通过接口统一克隆异构对象(这时才需要 Prototype 接口 + clone())。

4.3 需要运行期替换原型时

如果系统需要在运行期动态改变“接下来创建什么”,原型注册表是有价值的:

public class PrototypeRegistry {
    private final Map<String, Report> prototypes = new HashMap<>();

    public void register(String key, Report prototype) { prototypes.put(key, prototype); }
    public Report create(String key) { return prototypes.get(key).clone(); }
}

这时 clone() 的价值才真正显现:注册表只持有 Report 引用,create 不需要知道具体子类。这是 GoF 原型模式的本意——用实例替代类作为创建模板。

五、Python 实现:copy 模块把“深浅”问题摆到了明面上

5.1 标准库的两种复制

import copy

base = report_service.build(tenant_id, month)   # 昂贵,只做一次

pdf   = copy.copy(base)        # 浅拷贝:charts/template 共享
excel = copy.deepcopy(base)    # 深拷贝:完全独立

Python 的优势在于 “浅拷贝还是深拷贝”是调用方的显式选择,而不是隐藏在类实现里。copy.copy 和 copy.deepcopy 的语义清晰、命名直白,这是 Java clone() 做不到的。

但深拷贝也有自己的边界:

  • deepcopy 会递归复制一切,包括你不希望被复制的对象(数据库连接、线程锁、文件句柄)——它对这些对象要么报错,要么复制出无意义的副本;
  • 循环引用:deepcopy 用 memo 字典处理循环引用,不会无限递归,但复制出来的图结构与原图是两份独立的环;
  • 自定义 __deepcopy__:对于持有不可复制资源的类,必须实现 __deepcopy__ 来定义“什么该复制、什么该共享”。
class Report:
    def __init__(self, tenant_id, month, charts, template, db_conn):
        self.tenant_id = tenant_id
        self.month = month
        self.charts = charts
        self.template = template
        self._db_conn = db_conn          # 不应被复制的资源

    def __deepcopy__(self, memo):
        new = Report.__new__(Report)     # 不走 __init__
        new.tenant_id = self.tenant_id
        new.month = self.month
        new.charts = copy.deepcopy(self.charts, memo)
        new.template = self.template     # 不可变,共享
        new._db_conn = self._db_conn     # 连接共享,不复制
        return new

__deepcopy__ 是 Python 版的“手动深拷贝”——和 Java 里重写 clone() 做的是同一件事,但语义更明确:你是在定义“深拷贝时这个类怎么处理”,而不是在和 Cloneable 的历史包袱搏斗。

5.2 不可变对象:原型模式的“退化形态”

如果对象本身就是不可变的(frozen dataclass、NamedTuple),那“复制 + 修改”可以用 dataclasses.replace 表达:

from dataclasses import dataclass, replace

@dataclass(frozen=True)
class Report:
    tenant_id: str
    month: str
    charts: tuple[ChartData, ...]
    format: str = "pdf"

pdf   = base
excel = replace(base, format="excel")   # 只改一个字段,其余共享

这里没有显式的 clone(),但 replace 做的就是“复制 + 覆盖部分字段”。由于所有字段不可变,共享是安全的——原型模式的“复制”在不可变对象上退化成“结构共享”,成本几乎为零。这是函数式语言影响下的一种现代形态:用不可变性消灭“深拷贝还是浅拷贝”的问题本身。

5.3 GoF 版在 Python 里:注册表 + 可调用

_PROTOTYPES: dict[str, Report] = {}

def register(key: str, prototype: Report) -> None:
    _PROTOTYPES[key] = prototype

def create(key: str) -> Report:
    return copy.deepcopy(_PROTOTYPES[key])

和 Java 注册表版结构一致。Python 里更常见的变体是注册工厂函数而不是原型实例——但那已经属于工厂模式,不是原型模式了。原型模式的特征是“注册的是实例,创建靠复制”,这个特征要保留。

六、双语言对照表

维度 Java Python
典型实现 实现 Cloneable + 重写 clone();或拷贝构造函数 copy.copy / copy.deepcopy;__deepcopy__;不可变对象用 replace
深浅拷贝由谁决定 类实现者(重写 clone() 时手动处理) 调用方显式选择 copy 还是 deepcopy
不走构造函数的风险 super.clone() 跳过构造函数,不变量可能不成立 __deepcopy__ 常用 __new__ 绕过 __init__,同样要小心
继承链上的复制 子类必须重写 clone(),否则字段悄悄共享 deepcopy 自动处理整个对象图,子类字段默认被复制
不可复制资源(连接、锁) 需在 clone() 中显式处理 需实现 __deepcopy__ 或使用 copy 浅拷贝
样板代码量 较高,且容易写错 低(标准库承担大部分工作)
运行期替换原型 注册表 + Prototype 接口,较自然 注册表 + deepcopy,同样自然
主要风险 Cloneable 语义混乱、浅拷贝遗漏、子类未重写 deepcopy 复制了不该复制的资源、性能开销被低估
现代替代 拷贝构造函数、Record + 紧凑构造器、MapStruct dataclasses.replace、不可变模型、attrs.evolve

记忆锚点:原型 = 拿现成的当模板,复制出新的;它省掉的是构造成本,付出的是“复制语义必须想清楚”的负担。

七、现代视角:AI 生成复制逻辑时,审查什么

原型模式是 AI 最容易“生成一个能跑的 clone()、但复制语义完全错误”的模式——因为深浅拷贝的正确性依赖于领域知识(哪些字段可变、哪些资源不该复制),而 AI 看不到这些。

诊断阶段,先判断“复制”是不是真需求。 很多场景看似需要原型,实际需要的是不可变对象 + 结构共享(Python 的 replace、Java 的 record)——如果对象可以设计成不可变的,深拷贝问题根本不会出现。另一类场景需要的是参数对象重构(把 13 个字段拆成几个值对象,每个都可独立构造),而不是复制。AI 看到“创建昂贵”就会建议原型,但它不会先问“这个对象能不能做成不可变的”。

生成阶段,用复制语义而非模式名下指令。 “复制出来的报表与原报表在可变集合上完全独立,但数据库连接和模板配置必须共享,且复制不能重新拉取数据”——这个描述会迫使 AI 处理混合拷贝,而不是给一个裸的 super.clone() 或 copy.deepcopy。只说“用原型模式”,AI 大概率给出浅拷贝或全量深拷贝,两者都可能错。

审查阶段,原型的检查清单:

  1. 复制后的对象与原对象,在每一个可变引用字段上是否独立?逐个字段核对,不要相信“我用了 deepcopy”。
  2. 有没有字段是“不该被复制”的(连接、锁、缓存句柄、单例)?它们被正确处理了吗?
  3. 复制是否绕过了构造函数/__init__?如果有,类的不变量还能保证吗?
  4. 继承链上的子类字段,复制时被覆盖到了吗?
  5. 复制频率高吗?deepcopy 的开销是否被评估过?能否改用不可变对象 + 结构共享?

一个务实判断:在现代 Java 和 Python 里,原型模式的手写需求正在被两类东西吸收——一类是不可变数据模型(record、frozen dataclass、Pydantic),它们让“复制 + 修改”变成低成本的 replace/evolve;另一类是序列化/映射工具(MapStruct、copy.deepcopy、JSON 往返),它们承担了深拷贝的机械工作。手写原型真正还合理的场景是:需要在运行期用实例替换创建模板(注册表模式)、且对象图包含无法自动深拷贝的资源,必须自定义复制语义。

八、三十秒自测

  1. 浅拷贝和深拷贝的区别,在“对象持有 List 字段”这个例子上具体表现为什么?为什么 super.clone() 默认是浅的?
  2. 为什么说“复制绕过构造函数”是一把双刃剑?什么情况下这是优点,什么情况下是隐患?
  3. 在你的项目里找一处“为了生成变体而重复执行昂贵初始化”的地方,判断它该用原型复制、还是该先把对象改成不可变结构以支持廉价共享。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。