抽象工厂模式实现的dll插件功能. 收藏
[本人转载于:http://blog.csdn.net/s_51563946/archive/2009/08/18/4458480.aspx]
先谈下设计需求.一个可扩展的程序很多地方都希望是可以替换或者是可以扩展的.比如一个游戏引擎,希望渲染部分能够在D3D和OpenGL之间替换,希望shader部分能够在cg,hlsl,glsl之间切换,希望音频部分能够在DS3D和OpenAL之间切换,希望可以选择性的获得读取普通文件和读取zip或者其他输入流的能力,希望能够读取各种不同的模型文件格式的能力--而且这些都不必改变上层接口.
现在以"选择性的获得读取普通文件和读取zip或者其他输入流的能力"为例子来看看如果利用建立dll插件的方法来实现这一功能.
显然,我们是要构架一个文件系统.首先看看文件需要支持哪些功能:
view plaincopy to clipboardprint?
///文件输入流
class IFile
{
public:
///打开文件.
virtual bool open() = 0;
///关闭文件
virtual void close() = 0;
/**
从文件中读取指定长度的二进制内容
@param buffer 将文件内容读到的缓冲区
@param size 读出的尺寸,注意不要超过缓冲区的大小
@return 返回实际读出的尺寸
*/
virtual size_t read(void* buffer, size_t size) = 0;
/**
操作读指针的位置
@param off 指针的偏移量
@param stype 偏移的相对位置
*/
virtual void seekg(long off, FSE::Seek_Type stype = FSE::cur) = 0;
///取得当前读指针位置
virtual size_t tellg() = 0;
///判断文件指针是否在文件尾
virtual bool eof() const = 0;
///取得文件尺寸
size_t size() const;
///取得文件名
String file_name() const;
};
///文件输出流
class OFile
{
public:
///打开文件.
virtual bool open() = 0;
///关闭文件
virtual void close() = 0;
/**
向文件中写入二进制内容
@param buffer 待写入的内容
@param size 待写入的尺寸
@return 实际写如的尺寸
*/
virtual size_t write(const void* buffer, size_t size) = 0;
/**
操作写指针的位置
@param off 指针的偏移量
@param stype 偏移的相对位置
*/
virtual void seekp(long off, FSE::Seek_Type stype = FSE::cur) = 0;
///取得当前写指针位置
virtual size_t tellp() = 0;
///刷新缓冲区,将写缓冲区中的内容写到文件中
virtual void flush() = 0;
///取得文件名
String file_name() const;
};
///文件输入流
class IFile
{
public:
///打开文件.
virtual bool open() = 0;
///关闭文件
virtual void close() = 0;
/**
从文件中读取指定长度的二进制内容
@param buffer 将文件内容读到的缓冲区
@param size 读出的尺寸,注意不要超过缓冲区的大小
@return 返回实际读出的尺寸
*/
virtual size_t read(void* buffer, size_t size) = 0;
/**
操作读指针的位置
@param off 指针的偏移量
@param stype 偏移的相对位置
*/
virtual void seekg(long off, FSE::Seek_Type stype = FSE::cur) = 0;
///取得当前读指针位置
virtual size_t tellg() = 0;
///判断文件指针是否在文件尾
virtual bool eof() const = 0;
///取得文件尺寸
size_t size() const;
///取得文件名
String file_name() const;
};
///文件输出流
class OFile
{
public:
///打开文件.
virtual bool open() = 0;
///关闭文件
virtual void close() = 0;
/**
向文件中写入二进制内容
@param buffer 待写入的内容
@param size 待写入的尺寸
@return 实际写如的尺寸
*/
virtual size_t write(const void* buffer, size_t size) = 0;
/**
操作写指针的位置
@param off 指针的偏移量
@param stype 偏移的相对位置
*/
virtual void seekp(long off, FSE::Seek_Type stype = FSE::cur) = 0;
///取得当前写指针位置
virtual size_t tellp() = 0;
///刷新缓冲区,将写缓冲区中的内容写到文件中
virtual void flush() = 0;
///取得文件名
String file_name() const;
};
这是个简单的继承设计.比如我们要实现普通文件和zip文件的操作功能,我们只需要:
class Normal_IFile : public IFile
class Normal_OFile : public OFile
class Zip_IFile : public IFile
class Zip_OFile : public OFile
这样继承,然后分别实现其中的操作就好了.
但是这里需要注意到的是,zip文件并不只是单单包含一个压缩文件.一个zip文件可能是一个文档目录的压缩.因此我们还需要增加类:
view plaincopy to clipboardprint?
///文件目录类,代表一个文件目录
class Archive
{
public:
/**
取得此文档的类型.
@return:
如果返回为空串,表明为普通文档.其他
返回值由具体使用的文件系统插件决定.
例如使用PgeZipFileSys插件时,如果打
开的此文档是zip文档,则返回为"zip".
*/
virtual String type() = 0;
/**
在此目录下打开指定文件的输入流.
@param fileName 文件的相对路径与文件名
@return 返回文件指针,如果为NULL代表打开失败
*/
virtual IFile_SPtr open_input_file(const String& file_name) = 0;
/**
在此目录下打开指定文件的输出流.
@param fileName 文件的相对路径与文件名
@return 返回文件指针,如果为NULL代表打开失败
*/
virtual OFile_SPtr open_output_file(const String& file_name) = 0;
///取得目录名称
String archive_name();
};
///文件目录类,代表一个文件目录
class Archive
{
public:
/**
取得此文档的类型.
@return:
如果返回为空串,表明为普通文档.其他
返回值由具体使用的文件系统插件决定.
例如使用PgeZipFileSys插件时,如果打
开的此文档是zip文档,则返回为"zip".
*/
virtual String type() = 0;
/**
在此目录下打开指定文件的输入流.
@param fileName 文件的相对路径与文件名
@return 返回文件指针,如果为NULL代表打开失败
*/
virtual IFile_SPtr open_input_file(const String& file_name) = 0;
/**
在此目录下打开指定文件的输出流.
@param fileName 文件的相对路径与文件名
@return 返回文件指针,如果为NULL代表打开失败
*/
virtual OFile_SPtr open_output_file(const String& file_name) = 0;
///取得目录名称
String archive_name();
};
然后仍然按照继承的方法,
class Normal_Archive : public Archive
class Zip_Archive : public Archive
这样我们就有了一组相关的子类.这种情况正是使用抽象工厂模式的时机:
view plaincopy to clipboardprint?
///文档工厂,产生文档
class Archive_Facroty
{
public:
/**
打开一个目录.
@param dirname 待打开的目录路径
@param type 文档的类型
@return 返回此目录的Archive对象指针
*/
virtual Archive_SPtr open_archive(const String& dirname) = 0;
/**
返回该文档工厂的类型.注意务必保持和文档的类型名一致.
*/
virtual String type() = 0;
};
///文档工厂,产生文档
class Archive_Facroty
{
public:
/**
打开一个目录.
@param dirname 待打开的目录路径
@param type 文档的类型
@return 返回此目录的Archive对象指针
*/
virtual Archive_SPtr open_archive(const String& dirname) = 0;
/**
返回该文档工厂的类型.注意务必保持和文档的类型名一致.
*/
virtual String type() = 0;
};
然后再进行一次继承,分别产生普通目录的工厂和zip目录的工厂
class Normal_Archive_Facroty : public Archive_Facroty
class Zip_Archive_Facroty : public Archive_Facroty
这样设置后,就可以把不同类型(普通 or zip)的文档的创建责任放到一个对象上.这样就方便了我们下一个类的设计:File_System
view plaincopy to clipboardprint?
文件系统的抽象层
class File_System
{
/**
装载插件.
*/
void load_plugin(const String& plugin);
/**
添加搜索文档.
@param spath 搜索文档的名称
@param type 文档的类型,默认为空是指普通字符串.
其他类型由具体加载插件指定.例如zip
文件系统插件支持"zip"格式.
*/
void add_search_archive(const String& spath, const String& type = "");
/**
打开指定文档.如果已存在,则返回已存在的指针,否则
建立新的文档类.保证不会重复打开文件.
*/
Archive_SPtr open_archive(const String& arcname, const String& type = "");
/**
打开一个输入文件.
@note:
此函数首先会在已注册的搜索文档中按添加路径的顺
序对文档中的文件进行搜索,直到找到第一个文件为止.
@return
如果打开失败,将返回空指针
*/
IFile_SPtr open_input_file(const String& filename);
private:
//文档工厂列表
typedef std::list<Archive_Facroty*> Archive_Factory_List;
Archive_Factory_List m_arch_facty_list;
//文档类的观察指针
typedef boost::weak_ptr<Archive> Archive_VPtr;
//保存zip文档的映射表.键:文档路径名,值:文档类的观察指针
typedef std::map<String, Archive_VPtr> ArchiveMap;
ArchiveMap archives;
//搜索路径文档
typedef std::list<Archive_SPtr> Search_Archive_List;
Search_Archive_List m_search_archives;
};
文件系统的抽象层
class File_System
{
/**
装载插件.
*/
void load_plugin(const String& plugin);
/**
添加搜索文档.
@param spath 搜索文档的名称
@param type 文档的类型,默认为空是指普通字符串.
其他类型由具体加载插件指定.例如zip
文件系统插件支持"zip"格式.
*/
void add_search_archive(const String& spath, const String& type = "");
/**
打开指定文档.如果已存在,则返回已存在的指针,否则
建立新的文档类.保证不会重复打开文件.
*/
Archive_SPtr open_archive(const String& arcname, const String& type = "");
/**
打开一个输入文件.
@note:
此函数首先会在已注册的搜索文档中按添加路径的顺
序对文档中的文件进行搜索,直到找到第一个文件为止.
@return
如果打开失败,将返回空指针
*/
IFile_SPtr open_input_file(const String& filename);
private:
//文档工厂列表
typedef std::list<Archive_Facroty*> Archive_Factory_List;
Archive_Factory_List m_arch_facty_list;
//文档类的观察指针
typedef boost::weak_ptr<Archive> Archive_VPtr;
//保存zip文档的映射表.键:文档路径名,值:文档类的观察指针
typedef std::map<String, Archive_VPtr> ArchiveMap;
ArchiveMap archives;
//搜索路径文档
typedef std::list<Archive_SPtr> Search_Archive_List;
Search_Archive_List m_search_archives;
};
File_System有两个功能,一是通过load_plugin函数来装载不同的文档工厂,从而达到读写不同文件格式的能力.二是增加了搜索路径功能,可以方便的通过默认设置的搜索路径来打开文件.当然第二个功能在这里和整个设计结构关系不大.
这里要注意的是File_System中的文档工厂链表:
typedef std::list<Archive_Facroty*> Archive_Factory_List;
Archive_Factory_List m_arch_facty_list;
每次调用load_plugin,都会想这个链表中添加一个新项.这里属于一个责任链的设计模式,当使用open_archive函数的时候,文件系统会在Archive_Factory_List中查找类型匹配的文档工厂,让其负责该文档的产生.
//-----------------------------------------------------------------------------------
说到这里,整个系统就分成了3个工程.第一个主工程,其中包括了类:
File_System, Archive, Archive_Facroty, IFile和OFile.
第二个是普通文件的插件,这里假设工程将产生一个名为plugin_file_system_normal.dll的动态库,其中的类包括:
Normal_Archive, Normal_Archive_Facroty, Normal_IFile和Normal_OFile.
第三个是zip文件的插件,假设工程将产生一个名为plugin_file_system_zip.dll的动态库,其中的类包括:
Zip_Archive,Zip_Archive_Facroty,Zip_IFile和Zip_OFile.
//-----------------------------------------------------------------------------------
好了,到目前这个设计基本完成了,只剩下动态库插件跟主工程之间的粘连工作了.根据上面代码片段的注释,我们应该知道File_System::load_plugin函数的参数是dll的名称.那么这个函数内部所做的工作实际就是打开一个dll动态库,创建文档工厂对象.
为了能利用动态库创建文档工厂,我们需要为动态库添加一个导出函数.动态库的导出书写方式根据平台不同而不同.一般如果是windows平台上的动态库,导出函数前要加修饰前缀:__declspec( dllexport ).因此为了实现跨平台,通过宏定义隔离这些差别:
view plaincopy to clipboardprint?
//导出定义
//windows平台下
#if PLATFORM == PLATFORM_WIN32
#define Nor_FS_Export __declspec( dllexport )
//linux平台下
#else
#define Nor_FS_Export
#endif
//导出函数
extern "C" Nor_FS_Export Archive_Facroty* create_plugin()
{
return new Normal_Archive_Facroty();
}
//导出定义
//windows平台下
#if PLATFORM == PLATFORM_WIN32
#define Nor_FS_Export __declspec( dllexport )
//linux平台下
#else
#define Nor_FS_Export
#endif
//导出函数
extern "C" Nor_FS_Export Archive_Facroty* create_plugin()
{
return new Normal_Archive_Facroty();
}
在主程序一方也需要动态的打开动态库.而这些操作函数也是因操作系统不同而不同的.因此在这里也对其进行了封装.封装后接口如下(实现就不写了):
view plaincopy to clipboardprint?
class Dynamic_Lib
{
public:
///装载动态库
void load(const String& libname);
///卸载动态库
void unload();
///取得动态函数
void* get_process(const String& procname) const;
};
class Dynamic_Lib
{
public:
///装载动态库
void load(const String& libname);
///卸载动态库
void unload();
///取得动态函数
void* get_process(const String& procname) const;
};
以及对动态库进行管理的类:
view plaincopy to clipboardprint?
class Dynamic_Lib_Mgr
{
public:
/**
如果库还未装载,则装载此动态连接库,
否则返回此动态连接库的对象指针.
*/
Dynamic_Lib* load(const String& libname);
///根据库名卸载动态连接库
void unload(const String& libname);
};
class Dynamic_Lib_Mgr
{
public:
/**
如果库还未装载,则装载此动态连接库,
否则返回此动态连接库的对象指针.
*/
Dynamic_Lib* load(const String& libname);
///根据库名卸载动态连接库
void unload(const String& libname);
};
这个管理类主要是为了防止动态库被重复加载.
现在我们可以看下File_System::load_plugin的具体实现代码了:
view plaincopy to clipboardprint?
void File_System::load_plugin(const String& plugin)
{
LOG_MESSAGE("File system load a plugin", plugin, Log::LL_LOW);
Dynamic_Lib* lib = Dynamic_Lib_Mgr::instance().load(plugin);
typedef Archive_Facroty* (*lp_create_archive_factory)();
lp_create_archive_factory caf = (lp_create_archive_factory)lib->get_process("create_plugin");
m_arch_facty_list.push_back(caf());
}
void File_System::load_plugin(const String& plugin)
{
LOG_MESSAGE("File system load a plugin", plugin, Log::LL_LOW);
Dynamic_Lib* lib = Dynamic_Lib_Mgr::instance().load(plugin);
typedef Archive_Facroty* (*lp_create_archive_factory)();
lp_create_archive_factory caf = (lp_create_archive_factory)lib->get_process("create_plugin");
m_arch_facty_list.push_back(caf());
}
步骤分别是:写下装载插件的日志记录->打开动态库->创建create_plugin函数指针->从dll中取得该函数地址->使用该函数创建工厂对象并加到工厂链表中.
步骤分别是:写下装载插件的日志记录->打开动态库->创建create_plugin函数指针->从dll中取得该函数地址->使用该函数创建工厂对象并加到工厂链表中.
//-----------------------------------------------------------------------------------
现在要使用这个文件系统很简单:
File_System filesys;
filesys.load_plugin("plugin_file_system_normal.dll");//让文件系统获得普通文件的读写能力
filesys.load_plugin("plugin_file_system_zip.dll");//让文件系统获得Zip文件的读写能力
//添加搜索路径
filesys.add_search_archive("test/");
m_filesys.add_search_archive("test.zip", "zip");
IFile_SPtr file1 = m_filesys.open_input_file("1234.txt");//1234.txt在普通目录test/下
IFile_SPtr file2 = m_filesys.open_input_file("ReadMe.txt");//ReadMe.txt在test.zip里面
......
//-----------------------------------------------------------------------------------
这样就实现了通过插件选择指定能力的功能
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/s_51563946/archive/2009/08/18/4458480.aspx