泛化对象工厂简述
在C++的特性中,面向对象无疑是最受到关注的,面向对象是以继承以及虚函数为基础,他可以将行为延期至运行期,而且程序可以不必理会将运行的是哪一个子类成员函数,这就使得程序员在写程序时候有了很大的灵活性,这也是代码复用的基础。 例如下面的代码:
class A{};
class B:public A{};
class C:public B{};
A *point_1 = new B;
A *point_2 = new C;
这里代码就是经典的以基类指针指向继承类对象的用法,这可以使得以后直接对point_1操作,而不用管具体是对B还是C操作。不过似乎在实际程序中我们会遇到有些麻烦,就是我们这里生成对象时候必须明确的在程序中写出类B或者类C,也就使得灵活性大打折扣。
还有就是如果有一天你设计的程序并不知道是要生成B或者C类,也许你是把B将生成B或者C保存在数据库或者其他地方,知道你要产生对象的时候你才知道你具体是要生成数据库中指明的某个对象。乍看这似乎也并不难,只需要把访问数据库的语句写入程序,但是痛苦的是当你从数据库提取出你将要生成的类的时候却不可能是一个对象,而最大可能是一些字符串"B","C",这真是令人头痛的事情,因为你无法用字符串去生成一个对象。
对象工厂的产生就是要摆脱A *point_1 = new B;这种定式的生成语句,将对象的生成更智能化。
例如下面的句子
switch(chObject)
{
case 'B'
{
A *point_1 = new B;
break;
}
case 'C'
{
A *point_1 = new C;
break;
}
}
不过仅仅像上面那样是远远不够的,似乎你可以通过从数据库中获得字符以后生成对应的对象,但是C++多态的目的就是想让程序远离switch语句。
如果有一天随着客户的需求改变或者程序的壮大,你的程序需要添加更多多态对象的产生,那你就必须修改switch中的内容,修改代码也许不是噩梦,噩梦是你需要重新编译你的程序。
而泛化对象工厂是想给大家提供一个现成的且可以适应你不断变化的类的一种对象生成的模式。记住,是适应你的随时变化。让我们踏入神奇之旅吧。
(注:这一章思想主要来源【GoF】、【Modern C++ Design】,代码均为我自己编写。)
7.1.1 定义
提供一个接口用于创建对象,让子类决定实例化哪一个类且可以不用修改客户端代码。
7.1.2 泛化目的
传统的对象工厂模式(请参考【GoF】3.3工厂方法)是将对象的创建放入程序的判断语句当中,这使得在代码维护上增加了很多潜在的困难。
在这种C++灵活语言中希望得到一种能够灵活创建对象的方法,使得在对象增加或者减少时候不用对代码进行大量的修改维护。
7.1.3 适用场合
在下列情况下可以使用泛化对象工厂:
l 当一个类不知道它将要创建的对象的类的时候。
l 当可能创建的对象不断增加或者减少时候。
l 当你希望把创建对象名字交给数据库或者其他数据存储介质管理时候。
7.1.4 泛化对象工厂(Object Factories)初探
泛化对象工厂其实结构并不复杂,所以我们将慢慢介绍,但是单从外表来看,我们希望得到类似下面的代码:
ObjectFactories ObjFactor;
A *point_1 = ObjFactor.CreateObject("B");
char CreatTempName = 'C';
//需要注意的是还可以用变量代替了你要创建的对象的名字
A *point_2 = ObjFactor.CreateObject(CreatTempName);
我们需要创建某个对象完全可以通过数据库中的对象名创造对象,甚至希望可以通过运行期用户的需要创建对象。
7.2 泛化对象工厂的不足
感到幸运的是泛化对象工厂并不比传统的对象工厂低效,或者可以更高效,高效是体现在程序员开发的效率上,可以节约更多开发时间。
7.3实作泛化工厂
7.3.1 先来看看传统的对象工厂
举个例子,假设我们有一个图形编辑程序Shape,且我们将用到的所有不同的图形编辑都将继承这个Shape,Shape中定义了一些基本操作。
class Shape
{
public:
virtual void Draw() const = 0;
virtual void Rotate(double angle) = 0;
virtual void Zoom(double zoomFactor) = 0;
// ...and more...
};
而我们还有一个程序用来将这些图形装载至内存或者保存到硬盘(数据库)中,我们会写一个专门负责图形装载和保存的操作的Drawing类。
class Drawing
{
public:
void Save(std::ofstream& outFile);
void Load(std::ifstream& inFile);
//...and more...
};
其中的Save函数没什么特别的,只是将目前的所有图形对象保存而已。我们的注意应该在Load函数中,无论文件怎么保存,我们得到的数据都无法直接生成图形对象,必须经过程序的判断,将文件读取的对象字符串名字转换成对象(【Stroustrup 1997】)。
那我们的Load函数看起来大概是这样的:
void Drawing::Load(std::ifstream& inFile)
{
//读取文件中的对象名字
while(inFile)
{
int drawingType;
inFile >> drawingType;
//创建图形对象
Shape* pCurrentObject;
switch(drawingType)
{
case LINE: //直线
pCurrentObject = new Line; //Line继承自Shape
break;
case POLYGON: //圆
pCurrentObject = new Polygon;
break;
case CIRCLE: //多边形
pCurrentObject = new Circle;
break;
default:
//返回未定义图形的报错
}
}
}
上面Load函数中的switch语句就是典型的对象工厂模式,当你需要添加某个图形的时候你必须修改case代码。
让我们分析下Load到底是做了些什么。Load其实在文件中读取了对象的标示符,程序中用的是整型,无论是整型还是字符型都只是对象的一个标示符,然后通过代码的判断(switch、if)生成对应的对象。
说到底整个程序都是因为必须用到switch语句而显得很臃肿以及复杂。我们打算泛化对象工厂的第一步就是去掉switch。
7.3.2 STL(标准模板库)带来的便利
如果要去掉switch,我们就必须找到某种方法能够直接存储对象本身,且不知道这些对象具体是什么类。
还好事情并没有那么复杂,所有的图像编辑对象都必须继承Shape这个类,也就是说我们每个图像编辑对象的类型都可以是Shape。
其实在【C++程序设计新思维】中提到了解决的方法就是用STL中的关联容器来存储对象,关联容器中提供的映射表可以将对象关联到某个标示符上,这不正是我们需要的的吗。
关联容器中映射表结构是map(include <map>,参考【STL源码剖析】、【泛型程序设计与STL】),map内部是一棵RB-Tree(红黑平衡搜索二叉树)。所以在map内部并没有switch或者if的判断型句子。
有了map就解决了保存对象以及将对象映射到标示符的问题。但是绝大部分大学生对STL::map都不熟悉,所以下面我将简单介绍一下STL::map。
7.3.2.1 揭开STL::map神秘面纱
map是一种关联式容器,其可将类型为key的object和类型为T的object联系在一起,而这正是我们需要的东西,需要注意的是map中不允许出现key值重复的情况。在STL中用另外一种方式表示其类型key和T,STL::pair<const key,T>,STL::pair是一个存储两个object的小容器。还需要提到的是取用pair的第一个object是pair::first,取用第二个object是pair::second。
map有一个重要性质是向其插入一个元素不会使指向其中某个值的STL::iterator失效,这个与list性质是一样的。
现在来介绍一下map中我将会用到的成员函数。
l 构造函数:
map提供了两个构造函数版本,我们将使用0参数的够着函数,仅仅生成一颗空map树。
l 数据的插入:
map中插入数据会比较麻烦,在我的程序中用到的方法是调用
pair<iterator,bool> map :: insert(const value_type& x)
将x安插到map之中。如果x的确被安插了,返回值(pair)的第二部分为true,如果x已存在所以没有被安插,返回值(pair)第二部分为false。(参考【泛型程序设计与STL】)
上面提到的value_type定义于map之中
map::value_type
存储于此map之内的object,亦即pair<const key,T>
(参考【泛型程序设计与STL】)
所以整个安插动作是应该先将将插入的key和T赋值给value_type再调用insert函数
l 数据删除:
map提供了erase的两个重载版本。
void map::erase(iterator pos)
删除pos所指向的的元素。
size_type map::erase(const key_type& key)
删除所有“键值为key”的元素(如果存在的话),并返回被删除元素的个数。
(参考【泛型程序设计与STL】)
在我后面的程序中会用到第一个版本,虽然似乎第二个版本更好用,但是如果我们的类型T的object是用new构造的,单纯调用第二个删除函数就会造成内存溢出。所以必须先找出需要删除的object且调用delete后再调用map::erase。
l 查找函数:
map提供了两个find函数重载版本,但仅仅只是对const的符号的重载而已。
iterator map::find(const key_type& key)
寻找“键值为key”的元素。
const_iterator map::find(const key_type& key)const
寻找“键值为key”的元素。
(参考【泛型程序设计与STL】)
我们的程序中将调用第一个map::find版本。
其实map有时候并不是最好的选择,因为在我们实际应用中常常出现查找动作多于插入和删除动作的情况,这时候map就会显得比较低效,因为其中的RB-Tree不支持RandomAccess(随机访问)属性。我们可以试图写一个容器优化查找动作的效率(Loki::ObjectFactories中就采用其自己设计的容器),由于篇幅限制,这个将留给读者自己实现。
7.3.3 对象标示符
采用map有一个特点就是必须保证key不重复,这也是我们程序中必须保证的,因为object和其标示符key必须保证一一映射的关系,但是这就产生了一个问题,我们怎么能让程序自己生成不重复key且可以通过object逆向的获取这个key。
最简单的是我们采用一个计数器,给每个插入的object一个序号,每次这个序号分配以后就自加一,这样key就能自动产生。但是我们却不能通过object知道之前分配给其的key值是多少,而且计数器是一个很难维护的东西,当你删除一个obejct的时候,其key值是否被回收,如果被回收那将导致要一个复杂的登记方法,如果不回收就会是不久后计数器将膨胀到一个庞大的数。所以采用计数器生成key这个办法很快被我们否决了。
很快又有人提议用std::type_info这个class,其成员函数name()可以将object转换成字符串,这正好可以用作key,并且程序员可以不用维护key,因为这一切都将由std::type_info自动完成。不过一切也不是那么美好,std::type_info::name()原本是提供给程序员调试程序用的,所以其不能严格的根据object得到其名字,这一切都根据不同编译器而不同,这就不能给我们程序提供足够的稳定性,你不能想象本来在Windows上调试好的程序移植到linux上就必须重新修改程序。所以std::type_info也是不可行的。
在目前暂时没有办法根据object自动生成唯一且可逆的key的时候就只有由我们(程序员)手动管理key。如果由程序员自己管理key可以很容易的根据对象的名字命名key,又或者遵循团队的约定生成key。
在我的程序中将以object的名字字符串为key进行操作。
7.3.4 试探性实作ObjectFactories
很奇怪这一节的名字为什么是“试探性”,因为我总希望以我的视角将初级读者带入进来,所以我总是根据我的创作路线简述概念,而我的创作也不可能总是一马平川,曲折总是会有的,如果我省去讲述这些曲折会使初级的读者产生很多疑问“怎么会得到这样的,为什么不那样?”。所以这里我将先根据我最初的想法实作ObjectFactories,这个实作品很像【C++设计新思维】中的ObjectFactories的简化版,但是却具有相同的缺点,但是即便是名作【C++设计新思维】也对这些棘手的问题采取的回避的方法,不过幸运的是我找到了解决问题的方法,不过为了让初级读者能参与到实作的思考中,我将在后面部分对问题再介绍。
首先我们的ObjectFactories至少需要提供三个成员函数以及一个map成员变量。并且ObjectFactories需要两个模板参数,第一个是key的类型key_type,虽然这个在我的程序中建议使用object的名字字符串,但是泛化的含义迫使可以运用程序员自己定义key的类型;第二个是BaseType类型,是要产生对象的基类类型。
总体程序大概像下面这样的。
template<typename key_type,typename BaseType>
class ObjectFactories
{
//下面这个是将拿来存取继承类映射的容器
std::map<key_type,BaseType*> mAccess;
public:
//插入object
template<typename ObjectType>
bool Insert(const key_type& id,ObjectType* creator);
//删除object
bool Erase( const key_type& id );
//产生object并返回
BaseType* CreateObject(const key_type& id);
};
我想大家注意的是我们的插入函数的第二个参数是传入的指针而不是对象,这是因为只有将继承类的指针赋值给基类指针才是安全的,如果是单纯的将继承类对象赋值给基类对象将丢失很多信息。
bool ObjectFactories::Insert(const key_type& id,ObjectType* creator) //由于接收的是指针,所以插入时一定要用new创造对象,否则Erase函数时候会删除一个空对象将是很危险的
{
return mAccess.insert(std::map<key_type,BaseType*>::value_type(id,creator)).second;
//需要特别提到的是,这里可以将容器保存到数据库或者文件中,这样可以得到相当漂亮的结果
}
删除时候需要注意的是我们先前采用new传入的指针就必须在删除时候用delete释放掉。
bool ObjectFactories::Erase( const key_type& id )
{
typename std::map<key_type,BaseType*>::iterator iter = mAccess.find(id);
delete ite;
return mAccess.erase(id);
}
接下来就是返回对象了。
BaseType* ObjectFactories::CreateObject(const key_type& id)
{
typename std::map<key_type,BaseType*>::iterator iter = mAccess.find(id);
if( iter != mAccess.end()) //就像我现成设计的迭代器,这里他采用的是半闭半开区间,end()其实并不包含在容器里面,所以成了边界标志
{
return iter->second; //如果需要什么参数,可以参考我先前设计的仿函数,里面可以绑定参数,所以参数在这里不是问题
}
//这个地方写入你希望的报错方法,这时候是映射表里面没有查找的对象类型
else
{
BaseType* Err;
return Err;
}
}
从上面程序看到我们将映射表中的数据返回,这里有个问题就是我们怎么才能返回一个新创建的对象指针呢?由于我们将对象存储到map中,这也就导致我们失去了对象本身类型的信息,只知道对象的基类,但是基类类型对我们来说是没用的,所以我们目前也许只能返回先前存入的指针,但是如果当不同的指针都想获得同一个对象,那就将会出现一个麻烦就是不同指针指向同一块内存空间,这也就根本没有完成我们需要的“创建对象”的目的,而只是完成了“储存对象”的操作。
7.3.4 思考解决办法
在【C++设计新思维】中提到解决创建新对象的办法有两个,第一个是每个对象中的成员需要有一个clone()函数,函数返回本身的一个克隆体,程序代码类似这样:
class A
{
virtual A* clone()
{
return new *this;
}
};
当有继承类A的子类就必须改写其clone函数以返回自身对象。这样我们就可以每次通过调用其clone函数得到其对象的克隆体。这是可行的,现在的很多标准类都提供了clone函数,不过这也是局限的,因为你总不可能奢望每个类都提供了克隆函数。毕竟不是所有程序都达成这个标准。
还有一个办法是我们手头上有的是一个指向继承类的基类指针,虽然在我们程序里面看来new这个指针的对象只能创建一个基类对象,但是对于运行期的编译器却不是这样,运行期编译器始终能判断其具体类型,在那个时候我们再来产生相应的对象。这就是依靠RTTI提供的信息产生对象,不过这让程序复杂度和运行效率变得很沉重,并且type_info并不是那么好用。
【C++设计新思维】提到的这两个方法其实都不是很好,所以这本书没有实质性的解决这一问题,而是回避了这个问题。
幸运的是我想到了解决的方法,我们之所以不能产生对象是因为map使得我们缺失了很多信息,当指向所有对象的指针的存储到map得时候都将统一变成指向基类的指针。我们需要解决的就是我们能让map的每一个节点存储我们的对象指针,而不是用基类指针存储,这样能保存住我们需要的信息,但是map在初始化的时候必须要求用户特化出需要存储的数据类型并且一个map只能存储一个数据类型。所以我们需要自己做一个容器。
7.3.5 一个能够存储不同类型指针的容器
因为我曾经阅读过STL::map的源码,对其底层算法也算熟悉,所以最开始时候我本想同map一样用红黑二叉平衡搜索树来做容器,但是后来发现1000行的代码将给我的讲解带来很大的负担,所以我就选择了效率虽然低一点,但只有几十行代码的链表来做我们的容器。
链表中必然有两个变量需要存储,一个是我们的key,一个是key所对应的对象。而因为每个节点的key类型肯定都一样,而对象类型肯定不一样,不过对象都是继承自同一个基类,所以每个节点可以一个双层结构表示,部分灵感来自STL::list源码,因为其也采用的双层结构存储node,不过目的和我完全不一样,所以代码差别也很大。我经过了若干天的挣扎想到了下面的代码(实在无法向读者讲解挣扎过程,只能说是来自程序的创作灵感吧)。
template <typename KeyType ,typename BaseType>
struct Base_node
{
typedef BaseType* point;
Base_node *next; //next是指向其自身,所以就避开了继承类中实际是什么object
KeyType key_tmp ; //这里只是给下面的虚函数一个返回值,这个变量没有实际意义
//至于为什么不将key存储于这里的原因是我不想Base_node构造函数带参数
virtual KeyType get_key()
{
return key_tmp; //如果不返回将不能通过编译,但其实这里返回没有意义。
}
virtual point clone()
{
return 0;
};
};
template<typename KeyType,typename ValueType,typename BaseType>
class node : public Base_node<KeyType,BaseType>
{
public:
typedef ValueType Result;
Result temp; //这里是存储实际对象的位置,不过对于虚基类是未知了。
KeyType key; //这里是真正存储key的地方
typedef BaseType* point;
node(KeyType key_tmp):key(key_tmp){};
KeyType get_key()
{
return key;
}
//下面这个clone函数就是整个程序的精髓,很好的运用了多态。
point clone()
{
//object_clone只是一个简单的模板函数
return object_clone(temp);
}
};
template<typename T>
T* object_clone(T)
{
return new T;
}
像上面的这个节点代码很好的避开了存储对象的实际类型,因为每个节点指向下一节点的指针都是Base_node,而这个虚基类并没有携带对象的信息,这种技法在我们前面的泛化仿函数也见到过。
剩下的工作就是将每个节点封装成一个链表。
template <typename KeyType,typename BaseType>
class ObjectList
{
public:
typedef Base_node<KeyType,BaseType>* NodePtr;
NodePtr begin_node;
NodePtr end_node;
int length;
typedef BaseType* base_ptr;
//构造函数
ObjectList():begin_node(new Base_node<KeyType,BaseType>){end_node = begin_node;length = 0;}
//插入key和对象的映射关系
template<typename ValueType>
void insert(KeyType key,ValueType)
{
end_node->next = new node<KeyType,ValueType,BaseType>(key);
end_node = end_node->next;
++length;
}
int Length()
{
return length;
}
//删除一个key所对应的映射关系
bool erase(KeyType key)
{
NodePtr tmp_ptr;
tmp_ptr = begin_node;
int i=0;
while(i<length)
{
if(tmp_ptr->next->get_key() == key)
break;
tmp_ptr = tmp_ptr->next;
++i;
}
if(i==length)
return false;
else
{
NodePtr erase_ptr;
erase_ptr = tmp_ptr->next;
tmp_ptr->next = erase_ptr->next;
delete erase_ptr;
--length;
return true;
}
}
//清空整个链表
void clear()
{
NodePtr tmp_ptr;
tmp_ptr = begin_node;
int i=0;
while(i<length)
{
NodePtr erase_ptr;
erase_ptr = tmp_ptr->next;
tmp_ptr->next = erase_ptr->next;
delete erase_ptr;
++i;
tmp_ptr = tmp_ptr->next;
}
length = 0;
}
//创建并返回key映射之对象
base_ptr clone(KeyType key)
{
NodePtr tmp_ptr;
tmp_ptr = begin_node;
int i = 0;
while(i < length)
{
tmp_ptr = tmp_ptr->next;
if(tmp_ptr->get_key() == key)
break;
++i;
}
if(i==length)
return 0; //如果没找到映射关系就返回0
else
return tmp_ptr->clone();
}
};
上面都是常规的链表操作,比较简单,我就不一一阐述了。
通过上面的这些操作我们就可以操作一个链表,且链表每个节点可以存储一个不确定的对象。很漂亮的想法。
7.3.6 真正实作ObjectFactories
有了ObjectList的帮助,我们就可以轻易地实作出ObjectFactories了:
template<typename key_type,typename BaseType>
class ObjectFactories
{
//下面这个是将拿来存取继承类映射的容器
ObjectList<key_type,BaseType> mAccess;
public:
//下面这个是将映射关系插入容器的函数
template<typename ObjectType>
void Insert(const key_type& id,ObjectType)
{
return mAccess.insert(id,ObjectType()); //也可以是你自己设计的容器
//需要特别提到的是,这里可以将容器保存到数据库或者文件中,这样可以得到相当漂亮的结果
}
//下面这个是将映射关系删除的函数
bool Erase( const key_type& id )
{
return mAccess.erase(id);
}
BaseType* CreateObject(const key_type& id)
{
return mAccess.clone(id);
}
};
与原先的对象工厂不同的是我们这次储存的是对象实体而不是指针了,这就为我们克隆对象提供了足够的信息。
再来看看我们能怎么用对象工厂。
class Base
{
public:
virtual void print()=0;
};
class D1 :public Base
{
public:
void print()
{
cout<<"D1 was donging....flage1 = "<<this<<endl;
}
};
class D2 :public D1
{
public:
void print()
{
cout<<"D2 was donging....flage2 = "<<this<<endl;
}
};
int main()
{
ObjectFactories<char*,Base> ObjFactor;
ObjFactor.Insert("D1",D1());
ObjFactor.Insert("D2",D2());
Base *point_1 ;
point_1 = ObjFactor.CreateObject("D1");
char *TempName = "D1";
Base *point_2;
point_2 = ObjFactor.CreateObject(TempName);
point_1-> print();
point_2-> print();
getchar();
return 0;
}
我们的对象工厂总算大功告成,通过阅读这一章,希望传达一个信息给读者:当模板和面向对象相结合时候能写出许多精妙的代码。
7.4 摘要
对象工厂可以使得程序员摆脱古板的new创建对象。
l 泛化对象工厂可以将对象创建的时间无限推后直至用户运行期指定。
l 泛化对象工厂可以让增加或者减少key与对象间映射关系时候只需要“添加新代码”而不需要“修改已有代码”。
l 当你把存储映射关系的容器保存到文件时候或者从文件提取映射关系时候变得更为简便。