看到上图中的百元大钞、心里总在想这Money是我的那该多好。仔细看这些百元大钞,除了编号不同外,其余的信息都是一样的。印刷厂首先有一张100元的钞票,然后把它当作原型,印刷出一叠叠的钞票,流入市场。在设计模式中也存在一个类似的模式,可以根据一个原型对象克隆出多个一模一样的对象,该模式称之为原型模式。
1、原型模式
在使用原型模式时,我们需要首先创建一个原型对象,再通过复制这个原型对象来创建更多同类型的对象。如果先没有一张100元的钞票,那又如何产生出一大叠的百元大钞呢?原型模式的定义如下:
原型模式(Prototype Pattern):使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种对象创建型模式。 |
原型模式的工作原理很简单:将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝自己来实现创建过程。由于在软件系统中我们经常会遇到需要创建多个相同或者相似对象的情况,因此原型模式在真实开发中的使用频率还是非常高的。
需要注意的是通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,通常对克隆所产生的对象进行修改对原型对象不会造成任何影响,每一个克隆对象都是相互独立的。通过不同的方式修改可以得到一系列相似但不完全相同的对象。
原型模式的结构如图所示:
原型模式结构图
在原型模式结构图中包含如下几个角色:
Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。
ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
Client(客户类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。
2、刻录机的设计与实现
设计一个刻录机,目前要求能够刻DVD、VCD格式。现考虑可扩展性,将来可能还需要支持其它格式的刻录操作。
1.不使用模式实现方式
.h头文件代码如下:
#ifndef _RECORDE_H_
#define _RECORDE_H_
#include <iostream>
#include <string>
using namespace std;
//抽象刻录类
class Recorde
{
private:
string m_strContex;
public:
//设置需要刻录的内容
void SetContex(string strContex);
//获取刻录的内容
string GetContex();
};
//DVD类
class DVD : public Recorde
{
};
//VCD类
class VCD : public Recorde
{
};
#endif
cpp实现代码如下:
#include "Recorde.h"
//设置需要刻录的内容
void Recorde::SetContex(string strContex)
{
m_strContex = strContex;
}
//获取刻录的内容
string Recorde::GetContex()
{
return m_strContex;
}
Recorde为抽象刻录类,只包含设置刻录内容和获取刻录内容方法。DVD和VCD是Recorde的子类,负责对具体内容进行刻录操作。
测试代码如下:
#include <iostream>
#include "Recorde.h"
using namespace std;
int main()
{
//刻录第一张DVD
Recorde * pDVD1 = new DVD();
pDVD1->SetContex("DVD1");
//刻录第二张DVD
Recorde * pDVD2 = new DVD();
pDVD2->SetContex("DVD2");
//刻录第三张DVD
Recorde * pDVD3 = new DVD();
pDVD3->SetContex("DVD3");
//销毁操作
delete pDVD1;
pDVD1 = NULL;
delete pDVD2;
pDVD2 = NULL;
delete pDVD3;
pDVD3 = NULL;
return 0;
}
可以发现上面程序能够实现刻录操作、但程序是有问题的。刻录三张DVD,客户端需要显示的调用三次 new DVD()操作, 把创建的具体细节暴露给了客户端。如果需要大量的刻录操作,也需要大量地调用new操作创建各个对象。已经发现问题的所在,需要对刻录操作进行重构。在重构代码之前,在来看个不使用模式实现存在的问题。
2.不使用模式实现方式
上面的程序直接在客户端(Main函数)中刻录了三张DVD,现在封装一个刻录类,用来刻录DVD,VCD等格式,隐藏刻录的细节。
执行刻录操作类的实现过程如下:
#ifndef _RECORD_OPERATE_H_
#define _RECORD_OPERATE_H_
#include "Recorde.h"
//执行刻录操作类
class RecordOperate
{
public:
//动态的刻录DVD获取VCD
Recorde * ExcuteRecord(Recorde * pRecorde)
{
DVD * pDVD = NULL;
Recorde * pNewRecorde = NULL;
pDVD = static_cast<DVD *> (pRecorde);
if( NULL != pDVD )
{
string strContex = pRecorde->GetContex();
//需要知道具体刻录类型(违背迪米特法则)
pNewRecorde = new DVD();
//需要知道具体刻录类型的方法(违背迪米特法则)
pNewRecorde->SetContex(strContex);
return pNewRecorde;
}
else
{
string strContex = pRecorde->GetContex();
//需要知道具体刻录类型(违背迪米特法则)
pNewRecorde = new VCD();
//需要知道具体刻录类型的方法(违背迪米特法则)
pNewRecorde->SetContex(strContex);
return pNewRecorde;
}
}
};
#endif
测试代码如下:
#include <iostream>
#include "Recorde.h"
#include "RecordOperate.h"
using namespace std;
int main()
{
//DVD
Recorde * pDVD1 = new DVD();
pDVD1->SetContex("DVD1");
cout << pDVD1->GetContex() << endl;
//刻录DVD
RecordOperate * pRecordOperate = new RecordOperate();
Recorde * pDVD2 = pRecordOperate->ExcuteRecord(pDVD1);
cout << pDVD2->GetContex() << endl;
//VCD
Recorde * pVCD1 = new VCD();
pVCD1->SetContex("pVCD1");
cout << pVCD1->GetContex() << endl;
//刻录VCD
Recorde * pVCD2 = pRecordOperate->ExcuteRecord(pVCD1);
cout << pVCD1->GetContex() << endl;
//销毁操作
delete pDVD1;
pDVD1 = NULL;
delete pDVD2;
pDVD2 = NULL;
delete pVCD1;
pVCD1 = NULL;
delete pVCD2;
pVCD2 = NULL;
delete pRecordOperate;
pRecordOperate = NULL;
return 0;
}
编译并执行,结果如下:
ExcuteRecord函数带有一个抽象Recorde类型参数,用于执行刻录操作。但ExcuteRecord函数针对抽象Recorde类型进行编程,在程序执行过程中是无法判断具体需要刻录哪种类型的光盘(DVD还是VCD???),解决方法:使用运行时类型识别static_cast进行强制类型转化。
此时可以判断出需要刻录何种类型的光盘,但缺乏拓展性,如果需要刻录其它类型的光盘,得修改ExcuteRecord函数,违背了"开放---封闭原则"。
除违背了"开放---封闭原则"外,ExcuteRecord针对具体类型DVD或者VCD编程。pNewRecorde = new DVD()就是针对DVD这个具体类型进行编程,并且调用了DVD类型的SetContex方法。这违背了迪米特法则(最少朋友原则),即一个类只需要和自己的直接朋友打交道,同时尽可能少的调用朋友类的方法。RecordOerate类需要认识DVD类,VCD类,还得调用他们的SetContex方法。因此不能在RecordOerate中创建具体的DVD或者VCD,应该把创建的过程封装起来,由Record类自己负责创建一个对象,客户端就不需要知道具体的创建过程了。