Day 5:原型模式(Prototype Pattern)

创建型模式 · 第 5 天 / 共 23 个 GoF 模式 · 2026-08-17

学习路线位置:创建型模式收官之战。前四天分别解决了「全局唯一」「一个产品一个工厂」「一族产品配一个工厂」「分步构建复杂对象」,今天解决创建对象的最后一个难题——复制一个现成的对象。

① 今日主题

原型模式(Prototype Pattern):用一份已经创建好的对象当作「模板」(原型),通过 clone() 复制出一个个新对象。客户端既不需要写 new,也不需要知道具体类名,就能源源不断地获得「和原型一模一样」的独立实例。

一句话定义:让对象自己复制自己,把「如何复制」封装在对象内部,调用方只依赖抽象接口。

② 为什么需要这个模式?

先看一个真实场景:图形编辑器需要「复制粘贴」图形。没有原型模式时,复制逻辑只能写在客户端,靠类型判断 + 手工逐字段拷贝:

// 反例:没有原型模式时的"复制"逻辑
Shape* duplicateShape(const Shape& src) {
    if (src.type() == "Circle") {                       // 按类型分支
        const auto& c = static_cast<const Circle&>(src);
        auto* copy = new Circle(c.radius(), c.color());
        copy->setTags(c.tags());        // 手工逐字段复制,字段一多极易遗漏
        return copy;
    }
    if (src.type() == "Rectangle") {
        const auto& r = static_cast<const Rectangle&>(src);
        return new Rectangle(r.width(), r.height(), r.color());
    }
    // 每新增一种图形,这里就要加一个分支!
    return nullptr;
}

这段代码有四个问题:

需求继续增加(复制时还要复制动画、样式、子对象、撤销快照……),这个函数会越来越臃肿、越来越容易出错。根本原因:「复制」这件本该由对象自己负责的事,被外包给了客户端。于是我们需要原型模式——让每个对象自己实现 clone(),把复制逻辑收回对象内部。

③ 核心思想

原型模式的核心只有一句话:「复制」由对象自己完成,而不是由外部代码按类型手工拷贝。

每个可复制的对象实现一个 clone() 方法,返回一份「和它内容相同、但完全独立」的新对象。客户端要做的事只有一件:「给我一份和这个一样的」——它不需要关心对象到底是什么类、内部有多少字段、深拷贝还是浅拷贝,这些细节全部封装在对象自己内部。

生活类比:复印机。你把一份填好的表格放进复印机,按一下按钮,出来一份一模一样的——你不必知道表格是怎么排版印刷的,也不必重新填写一遍。又像细胞分裂:一个细胞复制出另一个细胞,新细胞自带全部内部结构。再想想 Word 的「另存为模板」:从一个做好的文档复制出无数新文档,每个再微调。

软件工程落点:clone() 本质上是「多态的构造函数」,也叫「虚拟构造函数」。普通构造函数是静态绑定的——你必须写出具体类名(new Circle(...))才能创建对象;而 clone() 是虚函数、动态绑定——你手里只有基类指针,调用 ptr->clone() 依然能复制出完整的派生对象。这就是原型模式最迷人的地方:创建动作(new)被推迟、被多态化,创建与使用彻底分离。

④ UML / 角色关系

原型模式只有三个核心角色(第四个是可选增强):

┌─────────────────────┐
│      Prototype      │  抽象接口:声明 clone()
│  + clone(): Proto   │  谁调用谁:Client 调用它
└──────────┬──────────┘
           ▲ 继承(实现 clone 的复制细节)
┌──────────┴──────────┐   ┌──────────────────────┐
│  ConcretePrototype  │   │  ConcretePrototype2  │
│  + clone(): Proto   │   │  + clone(): Proto    │
└──────────┬──────────┘   └──────────┬───────────┘
           │  clone() 返回自己的拷贝   │
┌──────────┴─────────────────────────┴───────────┐
│  Client(客户端)                                │
│  只依赖 Prototype 抽象,不依赖任何具体类          │
└────────────────────────────────────────────────┘

扩展点在哪里?新增一种可复制类型 = 新增一个子类 + 实现 clone(),既有代码一行都不用改——这正是开闭原则要求的形态。

⑤ 最小 C++ 示例

用「图形画布」演示:画布上要批量生成图形,全部通过克隆原型得到。C++17 完整可编译:

#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>

// 原型接口:定义"克隆"操作,所有可复制对象都继承它
class Shape {
public:
    virtual ~Shape() = default;
    virtual std::unique_ptr<Shape> clone() const = 0;  // 克隆:返回独立拷贝
    virtual void draw() const = 0;
};

// 圆形:具体原型
class Circle : public Shape {
public:
    Circle(int radius, std::string color)
        : m_radius(radius), m_color(std::move(color)) {}

    std::unique_ptr<Shape> clone() const override {
        // 用拷贝构造生成新对象;std::string 是值语义,自动深拷贝
        return std::make_unique<Circle>(*this);
    }

    void draw() const override {
        std::cout << "绘制" << m_color << "圆形,半径 " << m_radius << "\n";
    }

private:
    int m_radius;
    std::string m_color;
};

// 矩形:另一个具体原型
class Rectangle : public Shape {
public:
    Rectangle(int width, int height, std::string color)
        : m_width(width), m_height(height), m_color(std::move(color)) {}

    std::unique_ptr<Shape> clone() const override {
        return std::make_unique<Rectangle>(*this);
    }

    void draw() const override {
        std::cout << "绘制" << m_color << "矩形," << m_width << "x" << m_height << "\n";
    }

private:
    int m_width;
    int m_height;
    std::string m_color;
};

int main() {
    std::vector<std::unique_ptr<Shape>> canvas;  // 画布只持有抽象接口

    Circle redCircle(5, "红色");        // 原型 1:只创建一次
    Rectangle blueRect(3, 4, "蓝色");   // 原型 2

    // 客户端完全不关心具体类型,统一通过 clone() 复制
    canvas.push_back(redCircle.clone());
    canvas.push_back(redCircle.clone());   // 再复制一个红色小圆
    canvas.push_back(blueRect.clone());

    for (const auto& s : canvas) {
        s->draw();   // 多态调用,无需知道具体类型
    }
    return 0;
}

逐段解释:

程序输出:

绘制红色圆形,半径 5
绘制红色圆形,半径 5
绘制蓝色矩形,3x4

⚠ 深拷贝陷阱:本例成员都是值类型(int / std::string),默认拷贝构造就是深拷贝。但如果类里有裸指针成员(如 int* m_data),默认拷贝只是复制指针地址,两个对象会共享同一块内存——析构时双重释放、改一个全变。此时必须在 clone() 里手动深拷贝(new 一块新内存并拷贝内容)。现代 C++ 的最佳实践是用 RAII 容器(std::vector、std::string、std::unique_ptr)替代裸指针,让拷贝语义自动正确。

⑥ Qt 实战示例

做一个 Qt 6 图形小工具:左边工具栏每种图形维护一个「原型」,点击按钮就克隆一个新图形放进 QGraphicsScene——这正是 Qt 官方 Diagram Scene 示例复制粘贴功能的简化版,也是原型模式在 Qt 世界最典型的用法。

shapeitem.h(原型接口 + 两个具体原型):

#ifndef SHAPEITEM_H
#define SHAPEITEM_H

#include <QColor>
#include <QGraphicsItem>
#include <QPainter>
#include <memory>
#include <utility>

// 原型接口:所有可克隆图形项的抽象基类(也是 QGraphicsItem 子类)
class ShapeItem : public QGraphicsItem {
public:
    explicit ShapeItem(QColor color) : m_color(std::move(color)) {}
    ~ShapeItem() override = default;

    // 克隆接口:返回一份独立拷贝(多态复制)
    virtual std::unique_ptr<ShapeItem> clone() const = 0;

    QRectF boundingRect() const override { return QRectF(-30, -30, 60, 60); }

    void paint(QPainter* painter, const QStyleOptionGraphicsItem*, QWidget*) override {
        painter->setBrush(m_color);
        painter->setPen(QPen(Qt::black, 2));
        drawShape(painter);   // 由子类决定画什么形状
    }

protected:
    virtual void drawShape(QPainter* painter) const = 0;
    QColor m_color{Qt::white};   // 原型的状态:颜色
};

// 圆形:具体原型
class CircleItem : public ShapeItem {
public:
    explicit CircleItem(QColor color) : ShapeItem(std::move(color)) {}

    std::unique_ptr<ShapeItem> clone() const override {
        return std::make_unique<CircleItem>(*this);  // 拷贝构造 = 克隆
    }

protected:
    void drawShape(QPainter* painter) const override {
        painter->drawEllipse(QRectF(-25, -25, 50, 50));
    }
};

// 矩形:具体原型
class RectItem : public ShapeItem {
public:
    explicit RectItem(QColor color) : ShapeItem(std::move(color)) {}

    std::unique_ptr<ShapeItem> clone() const override {
        return std::make_unique<RectItem>(*this);
    }

protected:
    void drawShape(QPainter* painter) const override {
        painter->drawRect(QRectF(-25, -25, 50, 50));
    }
};

#endif // SHAPEITEM_H

main.cpp:

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QPushButton>
#include <QRandomGenerator>
#include <QVBoxLayout>
#include <QWidget>

#include "shapeitem.h"

int main(int argc, char* argv[]) {
    QApplication app(argc, argv);

    // 原型库:每种图形只保留一个原型,点击按钮即克隆出新实例
    CircleItem circleProto(Qt::red);
    RectItem rectProto(Qt::blue);

    QGraphicsScene scene;
    QGraphicsView view(&scene);
    view.setRenderHint(QPainter::Antialiasing);

    QPushButton btnCircle("克隆圆形");
    QPushButton btnRect("克隆矩形");
    QPushButton btnClear("清空场景");

    QWidget window;
    auto* layout = new QVBoxLayout(&window);
    layout->addWidget(&view);
    layout->addWidget(&btnCircle);
    layout->addWidget(&btnRect);
    layout->addWidget(&btnClear);

    // 点击"克隆圆形":从原型复制一个新圆并放入场景
    QObject::connect(&btnCircle, &QPushButton::clicked, &scene, [&] {
        auto item = circleProto.clone();   // ① 原型模式:克隆
        item->setPos(QRandomGenerator::global()->bounded(0, 300),
                     QRandomGenerator::global()->bounded(0, 300));
        scene.addItem(item.release());     // ② 场景接管所有权(Qt 负责 delete)
    });

    QObject::connect(&btnRect, &QPushButton::clicked, &scene, [&] {
        auto item = rectProto.clone();
        item->setPos(QRandomGenerator::global()->bounded(0, 300),
                     QRandomGenerator::global()->bounded(0, 300));
        scene.addItem(item.release());
    });

    QObject::connect(&btnClear, &QPushButton::clicked, &scene,
                     &QGraphicsScene::clear);

    window.resize(420, 480);
    window.show();
    return app.exec();
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(prototype_demo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Qt6 REQUIRED COMPONENTS Widgets)

qt_standard_project_setup()   # Qt 6 专用:自动处理 AUTOMOC 等

qt_add_executable(prototype_demo
    main.cpp
    shapeitem.h
)
target_link_libraries(prototype_demo PRIVATE Qt6::Widgets)

⑦ 代码执行流程

以 Qt 示例为例,完整运行流程:

  1. 启动阶段:程序构造两个原型对象 circleProto、rectProto——每种图形只在内存中存在一份「母本」。
  2. 用户操作:点击「克隆圆形」按钮,Qt 信号 clicked 触发 lambda 槽函数。
  3. 克隆发生:circleProto.clone() 被调用——由于虚函数动态绑定,实际执行 CircleItem 的拷贝构造函数,在堆上生成一个内容相同、内存独立的新对象(深拷贝 m_color 等成员)。
  4. 入场景:新对象被随机定位后 scene.addItem(item.release()) 交给场景,场景触发重绘,视图中出现一个新圆形。
  5. 循环往复:每次点击都从同一个原型克隆,互不影响;「清空场景」时场景统一 delete 所有克隆体——原型本身安然无恙,可继续克隆。

纯 C++ 示例的流程更简洁:创建原型 → clone()(拷贝构造 + 隐式转基类指针)→ 存入 vector<unique_ptr<Shape>> → 多态 draw() → 程序结束 RAII 自动释放全部对象。

⑧ 为什么这样设计?

⑨ 不使用会怎样?

没有原型模式,复制逻辑会退化成「类型分支 + 手工逐字段拷贝」的泥潭:

// 反例:类型分支复制,新增字段后极易漏拷贝
if (type == "Circle")     copy = new Circle(c.radius(), c.color());
else if (type == "Rect")  copy = new Rect(r.width(), r.height());
else if (type == "Line")  copy = new Line(...);
// 新加了一个"阴影"属性:这里忘了拷贝 → 静默 bug
// 第 6 种图形出现时,这个函数又要改一次

这些问题的根源都是同一个:复制责任放错了位置。把它收回对象内部(clone),一切迎刃而解。

⑩ 什么时候应该使用?

✅ 适合使用(3~5 个场景):

❌ 不适合使用(2~4 个场景):

⚠ 过度设计提醒:不要为了「炫技」给所有类加 clone()。判断标准很朴素——只有当代码里真的出现了「按类型写复制逻辑」或「需要复制运行时才知道类型的对象」时,才引入原型模式。设计模式是工具,不是装饰品。

⑪ 和其他模式的区别

最容易和原型混淆的是两个创建型模式,外加 C++ 自带的拷贝构造:

模式核心动作创建方式典型场景
原型 Prototype复制现有对象自我复制(clone),可保留当前状态复制粘贴、模板派生、运行时类型复制
工厂方法 Factory Method创建工厂子类决定 new 哪个具体类(从零创建)一个产品一个工厂,创建逻辑子类化
建造者 Builder组装分步设置参数,最后一步产出成品构造过程复杂、参数有顺序依赖

⑫ Qt 源码 / Qt 框架中的体现

⑬ 面试常见问题

Q1:原型模式和工厂方法模式有什么区别?

答:工厂方法由工厂决定「创建哪个具体类」,但仍是从零创建(出厂状态);原型模式由现有对象自我复制,客户端连「要创建什么类」都不需要知道,且复制品保留原型的当前状态。工厂适合首次创建,原型适合「基于实例再生成」。二者还可组合:注册表里的原型 + 工厂风格的对外接口。

Q2:C++ 里为什么需要 clone()?直接用拷贝构造函数不行吗?

答:拷贝构造是静态绑定。通过基类指针/引用拷贝派生对象时,只会调用基类的拷贝构造,派生部分被「切片」丢弃。clone() 是虚函数、动态绑定到实际类型,能复制出完整的派生对象;同时返回 unique_ptr,把所有权语义一并封装好。

Q3:clone() 什么时候必须做深拷贝?

答:当对象拥有独占资源(裸指针指向的堆内存、文件句柄等)时必须深拷贝,否则两个对象共享资源,析构时双重释放,或一处修改影响另一处。值语义成员(int、std::string、std::vector)默认拷贝即深拷贝,无需额外处理——这也是现代 C++ 鼓励用 RAII 容器替代裸指针的原因。

Q4:Qt 中 QGraphicsItem 的复制粘贴怎么实现?所有权如何管理?

答:为 QGraphicsItem 子类实现 clone() 返回 unique_ptr,克隆时深拷贝全部数据成员;加入场景时用 scene.addItem(item.release()) 把所有权交给场景,由场景负责销毁(Qt parent-child 机制)。注意:release() 之后绝不能再碰这个指针,所有权已转移。

Q5:原型管理器(Prototype Registry)有什么用?

答:用字符串名字登记一组原型(std::map<std::string, std::unique_ptr<Prototype>>),按名字取出克隆。它消除了客户端里「按类型决定创建方式」的 if/switch,配合配置文件或插件系统可实现「运行时才知道类型」的创建——这是原型模式区别于工厂模式的独有价值。

⑭ 今日练习(15~30 分钟)

需求:报表模板中心

  1. 定义抽象类 ReportTemplate:含 clone() 纯虚函数、标题 title、样式 style、数据行 std::vector<std::string> rows。
  2. 实现两个具体类 ExcelReport、PdfReport。
  3. 用 std::map<std::string, std::unique_ptr<ReportTemplate>> 做原型注册表,登记「月度销售报表」「年度总结报表」等模板。
  4. 实现 createReport(name, newTitle):从注册表克隆模板、修改标题、返回新报表;修改克隆体的数据行后,验证原型模板不受影响。

提示:① clone() 内写 std::make_unique<ExcelReport>(*this),rows 用 vector 自动深拷贝;② 注册表里存的是原型本身,取用时必须走 proto->clone(),千万不要把原型对象直接交出去。

⑮ 今日总结

一句话记忆:让对象自己复制自己(clone),客户端只认抽象接口——从此不再需要「按类型写复制代码」。

看到以下代码特征时,可以考虑原型模式:

明日预告:Day 6 适配器模式(Adapter Pattern)——结构型模式第一站。当两个接口「长得不一样」却必须协作时(老代码返回 const char*,新代码要 std::string;第三方库的接口和我们的接口不匹配),适配器如何在不修改任何一方的条件下「翻译」接口。与今天的原型(解决「如何复制对象」)不同,适配器解决「如何让不兼容的接口协作」。