原型模式:模式动机与定义

说实话,原型模式是我个人非常喜欢的一个创建型模式。为什么?因为它解决了一个很实际的问题——当你需要创建大量相似对象时,直接 new 的成本太高了。

我刚开始做 C++ 开发那会儿,接手过一个游戏引擎项目。里面要生成成千上万的粒子特效,每个粒子都有自己的位置、速度、颜色。如果每个粒子都走一遍构造函数,性能直接崩掉。后来我用了原型模式,效果立竿见影。

原型模式的动机其实很简单:通过复制一个已有对象来创建新对象,而不是通过实例化类。说白了,就是找个模板,直接拷贝一份。

它的定义是这样的:

原型模式(Prototype Pattern):用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。

嗯,这里要注意一点。原型模式的核心在于「拷贝」这两个字。但拷贝这件事,在 C++ 里可没那么简单。这就引出了我们下一个话题——深拷贝与浅拷贝。

深拷贝与浅拷贝

我在项目中遇到过不少因为拷贝问题导致的 bug。有一次,一个同事用默认拷贝构造函数复制了一个对象,结果两个对象共享了同一块内存。改了一个,另一个也跟着变了。排查了一下午才发现问题。

为什么会这样?因为 C++ 的默认拷贝是浅拷贝

浅拷贝

浅拷贝只复制对象本身,不复制对象内部指针指向的资源。两个对象的指针成员指向同一块内存。

class ShallowCopy {
public:
    int* data;
    ShallowCopy(int val) {
        data = new int(val);
    }
    // 默认拷贝构造函数 — 浅拷贝
    // ShallowCopy(const ShallowCopy& other) : data(other.data) {}
};

你想想看,如果其中一个对象析构时释放了 data,另一个对象的 data 就成了悬空指针。这就是典型的「双删」问题。

深拷贝

深拷贝则不同。它会复制对象本身,同时为指针成员分配新的内存,并复制指针指向的内容。

class DeepCopy {
public:
    int* data;
    DeepCopy(int val) {
        data = new int(val);
    }
    // 深拷贝构造函数
    DeepCopy(const DeepCopy& other) {
        data = new int(*other.data);
    }
    ~DeepCopy() {
        delete data;
    }
};

我个人习惯在实现原型模式时,一律使用深拷贝。除非你能百分之百确定对象内部没有动态资源,否则浅拷贝就是一颗定时炸弹。

警告:浅拷贝在多线程环境下尤其危险。我曾经因为浅拷贝导致两个线程同时修改同一块数据,程序崩溃得毫无征兆。

C++ 代码实现

好,我们来看看原型模式在 C++ 里怎么落地。核心思路是:基类提供一个 Clone() 纯虚函数,每个派生类自己实现深拷贝。

#include <iostream>
#include <string>
#include <vector>

// 抽象原型
class Shape {
public:
    virtual ~Shape() {}
    virtual Shape* Clone() const = 0;
    virtual void Draw() const = 0;
};

// 具体原型:圆形
class Circle : public Shape {
private:
    int radius;
    std::string color;
public:
    Circle(int r, const std::string& c) : radius(r), color(c) {}
    
    // 拷贝构造函数(深拷贝)
    Circle(const Circle& other) : radius(other.radius), color(other.color) {}
    
    Shape* Clone() const override {
        return new Circle(*this);
    }
    
    void Draw() const override {
        std::cout << "画一个" << color << "的圆形,半径:" << radius << std::endl;
    }
};

// 具体原型:矩形
class Rectangle : public Shape {
private:
    int width;
    int height;
    std::string color;
public:
    Rectangle(int w, int h, const std::string& c) 
        : width(w), height(h), color(c) {}
    
    Rectangle(const Rectangle& other) 
        : width(other.width), height(other.height), color(other.color) {}
    
    Shape* Clone() const override {
        return new Rectangle(*this);
    }
    
    void Draw() const override {
        std::cout << "画一个" << color << "的矩形,宽:" << width 
                  << ",高:" << height << std::endl;
    }
};

// 客户端代码
int main() {
    // 创建原型
    Circle circlePrototype(10, "红色");
    Rectangle rectPrototype(20, 15, "蓝色");
    
    // 通过克隆创建新对象
    Shape* shape1 = circlePrototype.Clone();
    Shape* shape2 = rectPrototype.Clone();
    Shape* shape3 = circlePrototype.Clone();  // 再克隆一个圆形
    
    shape1->Draw();
    shape2->Draw();
    shape3->Draw();
    
    // 清理
    delete shape1;
    delete shape2;
    delete shape3;
    
    return 0;
}

这段代码里,我用了拷贝构造函数来实现深拷贝。每个派生类都实现了自己的 Clone() 方法,返回 new Circle(*this) 或 new Rectangle(*this)。这样客户端只需要调用 Clone(),完全不用关心具体类型。

小技巧:如果你用 C++11 或更高版本,可以考虑返回 std::unique_ptr<Shape> 来自动管理内存。这样能避免手动 delete 的麻烦。

原型模式的知识体系

下面这张图可以帮你快速理解原型模式的核心结构:

原型模式结构图 Shape(抽象原型) + Clone(): Shape* Circle(具体原型) - radius: int - color: string + Clone(): Shape* Rectangle(具体原型) - width: int - height: int - color: string + Clone(): Shape* Client(客户端) 客户端通过 Clone() 创建新对象,无需知道具体类名

使用场景

原型模式不是万能的,但在某些场景下它确实是最优解。我根据自己的经验,总结了几个典型的使用场景:

场景 说明 我的经验
对象初始化成本高 构造函数需要加载文件、连接数据库等 游戏中的角色模板,加载一次后直接克隆
对象种类多且动态 运行时才能确定要创建什么类型 插件系统里,用原型注册表管理所有类型
避免工厂类层级爆炸 每个产品类对应一个工厂类,太冗余 用原型模式替代工厂方法,代码量减少一半
需要保存对象状态 快照、撤销操作等场景 编辑器里的撤销功能,用原型保存历史状态

核心要点:原型模式最适合「创建成本高、但复制成本低」的场景。如果你发现 new 一个对象要等好几秒,而拷贝只需要几毫秒,那原型模式就是你的菜。

我曾经在一个文档编辑器项目里用过原型模式。用户创建了一个复杂的表格,里面有各种格式、公式、样式。如果每次复制都要重新解析一遍,用户体验会非常差。我们用原型模式保存了一个「模板表格」,用户复制时直接克隆,速度提升了 10 倍以上。

嗯,最后提醒一句。原型模式虽然好用,但别滥用。如果你的对象很简单,new 一下就完事,那没必要搞这么复杂。设计模式是工具,不是目的。

避坑指南:我曾经在实现原型模式时,忘了处理对象内部的互斥锁、文件句柄等资源。这些资源不能简单拷贝,需要特殊处理。记得在 Clone() 方法里逐一检查每个成员变量。


公众号:蓝海资料掘金营,微信deep3321