原型模式

可能是最简单的创建型模式–原型模式

意图

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象

动机

想象一下以下几个场景

  1. 每次通过 new 产生一个对象成本非常高,比如依赖于高代价的数据库操作之后创建,而且用的频率还挺高
  2. 我拿到一个基类 Base 的指针, 它指向的是某一个派生类 Derived 对象, 我现在想要克隆这个指针,但是我的代码里不知道 Derived 的具体类型,也就没法直接调用构造函数

应用场景

借用《设计模式之禅》里提到的原型模式的应用场景:

  • 资源优化场景
    类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。
  • 性能和安全要求的场景
    通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
  • 一个对象多个修改者的场景
    一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑 使用原型模式拷贝多个对象供调用者使用。

其实总结起来就是: 创建新对象成本大,可以通过原型模式对已有对象进行克隆

此外:

  • 当要实例化的类是在运行时刻指定时,例如,通过动态装载
  • 为了避免创建一个与产品类层次平行的工厂类层次时
  • 当一个类的实例 只能 有几个不同状态组合中的一种时,建立相应数目的原型并克隆他们可能比每次用合适手工实例化该类更方便一些,当然这个可以结合抽象工厂或者简单工厂一起使用

UML图

UML

从 UML 图中可以看出, 客户端主要就是通过 p = prototype->Clone() 的形式获取一个克隆对象,整体的代码逻辑很简单,主要就是 Clone() 的实现

优缺点

  • 和所有的建造者模式一样,它对客户隐藏了具体的产品类,因此减少了客户知道的名字的数目,并且客户端不需要知道任何创建的细节
  • 对于创建有复杂或耗资源初始化过程的对象的时候,会提高创建效率
  • 创建的对象保存了原实例的所有状态
  • 基类 Base 的指针, 它指向的是某一个派生类 Derived 对象,获取这样指针的克隆时候,也格外具有优势

当然,它的缺点也很明显,为了保证新对象和原对象需要相互独立且一样,所以 Clone() 在类比较复杂的时候,保证实现 深拷贝 也就会变得很复杂了

来个栗子

在看 《设计模式之禅》 里关于原型模式的时候举到的一个例子我觉得很形象

一个对象的产生可以不由零起步,直接从一个已经具备一定雏形的对象克隆,然后再修改为生产需要的对象。
也就是说,产生一个人,可以不从1岁长到2岁,再到3岁……也可以直接找一个人,从其身上获得DNA,然后克隆一个,直接修改一下就是30岁了!

就依照上面的例子,但是我们稍微改一下,还是使用克隆人的概念,但是这时候,我们基类是 person (人), 派生类是 Chinese (中国人)

首先是设计一个 Prototype 的基类 person, 主要为这个人提供了年龄和姓名的设置,以及一个虚函数 clone()clone() 可以直接得到一个人的克隆对象

而C++在克隆这方面有个很简单实现,拷贝构造函数, 这个函数可以直接满足需求,一般情况下,在没有定义该函数的时候,C++的编译器会为我们定义一个默认的

#include "string"
using namespace std;

class Person
{
public:
    Person():m_year(0), m_name("") {}
    virtual ~Person(){}

    virtual Person *clone() { return new Person(*this); }

    void setYear(int year) { m_year = year;}
    int getYear() { return m_year;}

    void setName(string name) { m_name = name;}
    string getName() {return m_name;}

private:
    int m_year;
    string m_name;
};

接下来实现派生类 Chinese, 中国人有一个独有的属性,会使用的书法名称,比如楷书,隶书,小篆等等
因为这里的类里的成员比较简单,所以 clone() 的实现,直接调用了类的 拷贝构造函数

#include "person.h"

#include "string"
using namespace std;

class Chinese : public Person
{
public:
    Chinese(): Person(),m_character("楷书"){}
    ~Chinese(){}

    void setCharacter(string character) {m_character = character;}
    string getCharacter() {return m_character; }

    virtual Person *clone() { return new Chinese(*this); }

private:
    string m_character;   // 书法名称
};

最后看一下客户端部分的代码

#include "chinese.h"
#include <iostream>
using namespace std;

#define SAFE_DELETE(p) { if(p) { delete p; p=nullptr;}}
int main()
{
    cout << "初始化原实例,并对其赋值"<< endl;
    Person *p = new Chinese();
    p->setYear(30);
    p->setName("张三");
    cout << "原实例  年龄:"<<p->getYear() << ", 名称:" << p->getName() << endl;

    Person *p2 = p->clone();
    cout << "克隆原实例"<< endl;
    cout << "克隆体  年龄:"<<p2->getYear() << ", 名称:" << p2->getName() << endl;

    cout << "对克隆体重新赋值"<< endl;
    p2->setYear(20);
    p2->setName("李四");
    cout << "原实例  年龄:"<<p->getYear() << ", 名称:" << p->getName() << endl;
    cout << "克隆体  年龄:"<<p2->getYear() << ", 名称:" << p2->getName() << endl;

    SAFE_DELETE(p);
    SAFE_DELETE(p2);
    return 0;
}

运行结果

初始化原实例,并对其赋值
原实例  年龄:30, 名称:张三
克隆原实例
克隆体  年龄:30, 名称:张三
对克隆体重新赋值
原实例  年龄:30, 名称:张三
克隆体  年龄:20, 名称:李四

上面的例子是 pPerson 基类对象,但是指向的是基类 Chinese, 为了确定克隆出的对象是否具有 ChineseCharacter(书法字体) 属性, 需要进一步测试,代码如下:

#include "chinese.h"
#include <iostream>
using namespace std;

#define SAFE_DELETE(p) { if(p) { delete p; p=nullptr;}}
int main()
{
    cout << "初始化原实例,并对其赋值"<< endl;
    Chinese *p = new Chinese();
    p->setYear(30);
    p->setName("张三");
    p->setCharacter("小篆");
    cout << "原实例  年龄:"<<p->getYear() << ", 名称:" << p->getName() << ", 书法:" << p->getCharacter() << endl;

    Person *p2 = p->clone();
    if (auto p3 = dynamic_cast<Chinese*> (p2) ) {
        cout << "克隆原实例"<< endl;
        cout << "克隆体  年龄:"<<p3->getYear() << ", 名称:" << p3->getName() <<  ", 书法:" << p3->getCharacter() << endl;

        cout << "对克隆体重新赋值"<< endl;
        p3->setYear(20);
        p3->setName("李四");
        p3->setCharacter("隶书");
        cout << "原实例  年龄:"<<p->getYear() << ", 名称:" << p->getName() << ", 书法:" << p->getCharacter() << endl;
        cout << "克隆体  年龄:"<<p3->getYear() << ", 名称:" << p3->getName() <<  ", 书法:" << p3->getCharacter() << endl;
    }

    SAFE_DELETE(p);
    SAFE_DELETE(p2);

    return 0;
}

运行结果如下:

初始化原实例,并对其赋值
原实例  年龄:30, 名称:张三, 书法:小篆
克隆原实例
克隆体  年龄:30, 名称:张三, 书法:小篆
对克隆体重新赋值
原实例  年龄:30, 名称:张三, 书法:小篆
克隆体  年龄:20, 名称:李四, 书法:隶书

从例子我们看出,通过 clone() 得到的克隆体,保证了克隆体和原对象状态的一致性,不用重新初始化对象,而是动态的获得了对象运行时的状态

总结

如果使用构造函数创建类,那么这个类对客户端而言是已知的,原型模式设计的类可以是未知的,调用的是 clone(), 利用C++多态的特性,保证和原对象一样的属性。

如果细心的阅读了本篇文章,例子中 Clone() 部分我说过,类成员比较简单,可以直接使用缺省的 拷贝构造函数, 但是这个简单指的是,类的成员中没有指针,类类型成员变量中也没有指针,可是实际代码中这些可能都有。而关于 拷贝构造函数 这一块的总结,我放在接下来的这篇文章中,这篇文章有较为详细的介绍

原型模式 的总结就这些了,不知不觉将设计模式里,创建型的都总结完了~