2.1模式动机

在有些系统中,存在大量相同或相似对象的创建问题,如果用传统的构造函数来创建对象,会比较复杂且耗时耗资源,用原型模式生成对象就很高效,就像孙悟空拔下猴毛轻轻一吹就变出很多孙悟空一样简单。

《设计模式》第二部分 创建型设计模式 第6章 原型模式(A:C++实现)_创建对象

图1

再举一个最简单的例子来说明原型模式:记得上小学的时候,老师把需要做的课外习题写到黑板上,而下面的我们都要把这些题抄写到自己的本子上,回家做好,第二天交上来,也就是每道题,全班50个人,每个人都要抄写一遍。按照现在的时间理论来说,就是浪费了50个人的时间。但是,那个时候条件限制,老师也是不得已而为之。现在好了,老师做一份电子版的习题,打印一份,然后拿着这份打印的原版,就可以复制出50份。

老师打印出来的那一份,就是“原型”,而复制出来的那50份,就是使用的“拷贝”。而原型模式就是这么简单的一个道理,通过现有的东西,再复制出一个来。

好了,接下来,我们正式进入理论部分。

2.2模式的定义与特点

《设计模式:可复用面向对象软件的基础》中是这样说的:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

原型(Prototype)模式的定义如下:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。在这里,原型实例指定了要创建的对象的种类。用这种方式创建对象非常高效,根本无须知道对象创建的细节。例如,Windows 操作系统的安装通常较耗时,如果复制就快了很多。在生活中复制的例子非常多,这里不一一列举了。

原型模式说白了其实就是有一个把自己拷贝一下的方法。该模式很好理解,该模式独特地方不是类与类之间的关系,更多的是从语义上理解,只是实现了一个接口而已。

2.3模式的结构与实现

2.3.1模式的结构

原型模式包含以下主要角色。


 抽象原型类:规定了具体原型对象必须实现的接口。
 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
 访问类:使用具体原型类中的 clone() 方法来复制新的对象。


其结构图如图 2所示。

《设计模式》第二部分 创建型设计模式 第6章 原型模式(A:C++实现)_创建对象_02

图2原型模式的结构图

由于克隆需要一个原型,而上面的类图中Prototype就这个原型,Prototype定义了克隆自身的Clone接口,由派生类进行实现,而实现原型模式的重点就在于这个Clone接口的实现。ConcretePrototype1类和ConcretePrototype2类继承自Prototype类,并实现Clone接口,实现克隆自身的操作;同时,在ConcretePrototype1类和ConcretePrototype2类中需要重写默认的复制构造函数,供Clone函数调用,Clone就是通过在内部调用重写的复制构造函数实现的。在后续的编码过程中,如果某个类需要实现Clone功能,就只需要继承Prototype类,然后重写自己的默认复制构造函数就好了。好比在C#、Java等于语言中就提供了clone接口,当某个类需要实现原型模式时,只需要实现这个接口的道理是一样的。不管用什么面向对象的语言,思想都是一样的。我们在这里学的是思想。

2.3.2模式的实现

/**Includes*********************************************************************/
#include <iostream>

/**namespace********************************************************************/
using namespace std;

//接口
class Prototype
{
public:
Prototype(){}
virtual ~Prototype(){}

virtual Prototype *Clone()=0;
};

//实现
class ConcretePrototype_1 : public Prototype
{
public :
ConcretePrototype_1():m_counter(1){}
//拷贝构造函数
ConcretePrototype_1( int counter)
{
this->m_counter = counter;
}

//复制自身
virtual Prototype * Clone()
{
//调用拷贝构造函数
return new ConcretePrototype_1(*this );
}
void Show()
{
cout << this->m_counter << endl;
}

virtual ~ConcretePrototype_1(){}

private :
int m_counter;
};

//实现
class ConcretePrototype_2 : public Prototype
{
public :
ConcretePrototype_2():m_counter(2){}
//拷贝构造函数
ConcretePrototype_2(int counter)
{
m_counter = counter;
}

//复制自身
virtual Prototype * Clone()
{
//调用拷贝构造函数
return new ConcretePrototype_2 (*this );
}

void Show()
{
cout << this->m_counter << endl;
}

virtual ~ConcretePrototype_2(){}

private :
int m_counter;
};

/**
* @brief 主函数
* @param argc
argv
* @retval None
*/
int main(int argc, char *argv[])
{
ConcretePrototype_1 * p1 = new ConcretePrototype_1();
ConcretePrototype_2 * p2 = (ConcretePrototype_2 *)(p1->Clone());

p1->Show();
p2->Show();

delete p1;
delete p2;

return 0;
}

结果如下所示:

《设计模式》第二部分 创建型设计模式 第6章 原型模式(A:C++实现)_设计模式_03

上述代码实现了一个最简单的原型模式,但是已经将原型模式的基本实现原理展现出来了。而有的时候,当调用Clone获得了一个复制的对象以后,需要改变对象的状态,此时就可能需要在ConcretePrototype类中添加一个Initialize操作,专门用于初始化克隆对象。由于在Clone的内部调用的是复制构造函数,而此处又涉及到深复制和浅复制的问题。所以,在实际操作的过程中,这些问题,都需要进行仔细的考虑。

2.4模式的应用场景

原型模式和建造者模式、工厂方法模式一样,都属于创建型模式的一种。简单的来说,我们使用原型模式,就是为了创建对象。但是,在以下场景下,使用原型模式是最好的选择:


  • 当我们的对象类型不是开始就能确定的,而这个类型是在运行期确定的话,那么我们通过这个类型的对象克隆出一个新的对象比较容易一些;
  • 有的时候,我们需要一个对象在某个状态下的副本,此时,我们使用原型模式是最好的选择;例如:一个对象,经过一段处理之后,其内部的状态发生了变化;这个时候,我们需要一个这个状态的副本,如果直接new一个新的对象的话,但是它的状态是不对的,此时,可以使用原型模式,将原来的对象拷贝一个出来,这个对象就和之前的对象是完全一致的了;
  • 当我们处理一些比较简单的对象时,并且对象之间的区别很小,可能就几个属性不同而已,那么就可以使用原型模式来完成,省去了创建对象时的麻烦了;
  • 有的时候,创建对象时,构造函数的参数很多,而自己又不完全的知道每个参数的意义,就可以使用原型模式来创建一个新的对象,不必去理会创建的过程,让创建过程见鬼去吧。

所以,在上述的的情况下,在设计的时候,适当的考虑一下原型模式,减少对应的工作量,减少程序的复杂度,提高效率。

2.5模式的扩展

原型模式可扩展为带原型管理器的原型模式,它在原型模式的基础上增加了一个原型管理器 PrototypeManager 类。该类用 HashMap 保存多个复制的原型,Client 类可以通过管理器的 get(String id) 方法从中获取复制的原型。其结构图如图3所示。

《设计模式》第二部分 创建型设计模式 第6章 原型模式(A:C++实现)_C++_04

图3带原型管理器的原型模式的结构图

2.6总结

工厂方法模式、抽象工厂模式、建造者模式和原型模式都是创建型模式。工厂方法模式适用于生产较复杂,一个工厂生产单一的一种产品的时候;抽象工厂模式适用于一个工厂生产多个相互依赖的产品;建造者模式着重于复杂对象的一步一步创建,组装产品的过程,并在创建的过程中,可以控制每一个简单对象的创建;原型模式则更强调的是从自身复制自己,创建要给和自己一模一样的对象。

原型模式作为创建型模式中最特殊的一个模式,具体的创建过程,是由对象本身提供,这样我们在很多的场景下可以很方便的快速的构建新的对象。但是,原型模式的最大缺点是继承原型的子类都要实现Clone操作,这个是很困难的。例如,当所考虑的类已经存在时就难以新增Clone操作。当内部包括一些不支持拷贝或者有循环引用的对象时,实现克隆可能也会很困难。说以说,每一种设计模式都有它的优点和缺点,在设计的时候,我们需要进行权衡各方面的因素,扬长避短。