从0到1构建自己的插件系统–接口定义与类对象的实现逻辑
上篇文章我们简述了自主插件的特点以及基本的流程,本文着重讲插件化框架的基本实现策略
看看插件的类对象如何使用
没有使用过COM 组件开发的研发人员,可能对于组件的强大是没办法理解的。那么我们先来看看一个简单的例子
Object<ISample> sample_class(CLSID_SAMPLE);
sample_class->add(2,4);
非常简单的一段代码,初步看起来没有什么特点,看起来像是一个智能指针类的创建方式,那么它到底有何强大之处呢。
Object 确实是一个智能指针模板类来的,这个类的构造函数传入一个CLSID_SMAPLE的标记,这个标记其实可以理解为一个类的唯一标识。同时我们注意的是ISample是一个接口(一般接口的名字都是I开头),不是可实例化的类对象)并不是调用的实现类的对象(一般的智能指针是实现类名称),但是执行这个之后却可以调用Smaple类的插件执行类对象功能,但没有对smaple类有任何的依赖,也不需要事先加载动态库。这个就是COM组件的强大的之处。
使用这种方式创建类对象,具有以下的优点
- 隐藏了实现类定义,暴露的只是接口。这样我们就不怕别人从一些细节看出来这个类使用了什么核心技术,写的差也没有人知道,完全隔离。
- 对于主程序和其他插件而言,使用的时候不用关心依赖了哪些库(对于新人来说太方便了,从此跟各种未定义错误拜拜)。
- 编译时间提高50%以上,对于改变类的实现、添加私有函数、成员变量而言,只需要编译自身库即可。
- 如果别人对于自己的某个实现不满意,完全可以自己写一个替换掉,不用担心对系统造成什么影响。
- 容易建立生态,增加某个模块或者禁用某个模块,提升系统运行效率都非常的方便
理解接口
接口是一组以函数逻辑的集合,对于插件接口而言,接口限定了插件与使用该插件程序或者其他插件的交互方式,任何具备同一接口的组件都可以对此插件进行相当于其他插件的透明的替换。只要接口不发生变化,就可以在不影响整个由组件构成的系统的情况下自由的更换组件。通常在程序设计阶段需要将接口设计的尽可能完美,以减少在开发阶段对COM接口的更改。尽管如此,在实际应用中是很难做到这一点的,往往需要在现有接口基础上对其做进一步的发展。
Com组件的接口IUnkown
接口 IUnkown
实际上是一个只包含纯虚函数的类,所有的接口都继承 Iunkown
对于插件/组件而言,暴露在外面的一定是接口,不能是可创建的类对象,接口文件是在单独的头文件中,如IUnkown接口定义在 IUnkown
头文件中。
class IUnkown
{
public:
virtual HRESULT QueryInterface(const IID& id,void**ppv)=0;//查询接口是否符合某个规范
virtual ULong AddRef()=0;//智能指针的引用计数
virtual ULong Release()=0;
};
接口规范
- 接口必须具有唯一的一个ID(接口ID,可以取类名的HASH值来实现)
- 所有的接口必须直接或者间接继承Iunkown
- 接口不要使用多继承(容易出问题)
- 接口中函数的参数建议不要用STL类库(由于跨编译器需要),如果确定只用所有插件只用一种编译器,则不存在这类问题
- 接口中能够通过返回值返回值返回的值尽量通过返回值来返回
自主插件接口定义
既然我们知道了基本原则,知道如何定义接口,那么我们可以仿照COM组件的接口定义一套自己的接口,但是因为要考虑跨平台,接口的参数不要带跟平台有关的对象变量
class IUnkown
{
public:
virtual int addRef()=0;
virtual int release()=0;
virtual bool queryInterface(const long& id,void** ppv)=0;
};
//如何自定义的接口需要直接或者间接继承Iunkown接口
class ISample:public IUnkown
{
public:
virtual int add(int x,int y)=0;
};
最简单的一个接口的定义就完成了,但是这样还是不够的,因为我们还不知道哪个地方可以获取接口的ID和类名,所以我们需要加上去,为了简单我们添加一个接口定义宏 REGISTER_INTERFACE
#define REGISTER_INTERACE(class_name,super_class) \
public:\
static const char* className() {return ##class_name;} \
static int classID const(){return _hashvalue(##class_name);}\
public:
class IUnkown
{
REGISTER_INTERFACE(IUnkown)
public:
virtual int addRef()=0;
virtual int release()=0;
virtual bool queryInterface(const long& id,void** ppv)=0;
};
class ISample:public IUnkown
{
REGISTER_INTERFACE(ISample) //添加一个宏定义,将基本信息封装到这个宏中;
public:
virtual int add(int x,int y)=0;
};
//其中_hashvalue函数实现为
inline long _hashvalue(const char *str)
{
unsigned long value = 0;
while(*str)
{
value = (value << 5) + value + *str++;
}
return value;
}
对象类定义
接口的定义是为了让接口和实现类做一个分离,解决在编译期间对插件LIB库的编译依赖,也无需对实现类进行类导出说明,在定义一个类之前,我们需要思考几个问题。
- 对象类的实现是否可以继承多个接口?
一般情况下认为是不可以的,多继承会涉及到虚继承问题,导致对接口的继承有限制,会比较麻烦。但是在这里是没有问题的,因为这个实现类的定义还不是最终的实现类,它属于一个中间类。
- 插件由于隐藏了实现,那么如何创建一个类?
解决办法有两个:一是通过抽象工厂的设计模式,将类注册到工厂中,通过简单的类名查找需要创建的类对象,但是这种方式的抽象工厂一般都是对于一个特定接口的设计,不太适合;二是通过回调函数来实现,为每个类创建一个静态函数,将此函数地址存储在类的基本信息中。
- 如何通过接口查询一个类?
那肯定是需要将接口的ID和类的ID建立一个关联关系
#define CLASS_DEFINE(class_name,super_class) \
public:\
static const char* className() {return ##class_name;} \
static int classID const(){return _hashvalue(##class_name);}\
\\C++没有反射,当前类与接口的关系需要通过函数clisdList来解决,返回值是一个接口ID的数组,对于多接口返回多个接口的列表;
static long[] clisdList() {return long id_list[1]; id_list[0]=super_class::classID();return id_list;} \
\\查询接口是否符合某个接口ID
static bool doQuery(cls* self, long iid, IUnkown** ppv) { \
auto clisd_list=clisdList();\
for(int i=0;i<sizeof(clisd_list);++i) {\
if(clisd_list[i]==iid) {*ppv= static_cast<_Interface*>(self); return true;} else {return false;} } \
}; public:
class Sample:pubic ISample
{
CLASS_DEFINE(Sample,ISample) //对于实现类需要加上这个宏定义;
public:
int add(int x,int y) ovrride
{
return x+y;
}
};
CLASS_DEFINE
宏使用一种简单的方式解决了获取类的接口信息问题,但不是最好的解决方案(如果中间增加一层接口,那么从中间这个接口直接创建类失败的,如IUnkown->ISample1->ISample2->Sample(实现类) ,那么从ISample这个接口去查询实现类肯定是不行的,因为(doQuery)查询失败。我在后来的版本中已经解决了这个问题,这里只是说明下实现类的定义方法,如果想要完整的解决方案,请查看教程 如何解决类对象的多层继承问题
完整插件的类继承图
很多同学可能注意到了,前面 Sample
这个类是没有办法矢量化出来的,这是因为基类Iunkown的没实现呢。那么我们是不是加上这两个实现就可以了呢,答案是没有简单。我们还需要一个下层类,对 IUnkown
的接口进行实现 ,这个类是个模板类,继承Samp类,NormalObject 才是我们最终要注册的类。
//Cls是用户的实例类,以下代码属于伪代码,该类在头文件 normalobject.hpp头文件中;
template <class Cls>
class NormalObject : public Cls
{
protected:
NormalObject()
{
++_ref_count;//初始化时引用计数为1
}
virtual ~NormalObject()
{
}
protected:
//IUnkown的实现;
virtual bool queryInterface(long iid, IObject **p)
{
return Cls::doQuery(this, iid, p);
}
//IUnkown的实现;
virtual const char* getClassName()
{
return Cls::className();
}
//类的引用计数,实现智能指针用,与shared_ptr不同,这个是侵入式的;
virtual long addRef()
{
return ++_ref_count;
}
virtual long deref()
{
if(_ref_count<=0)
{
return 0;
}
if(0== --_ref_count)
{
delete this;
return _ref_count;
}
return _ref_count;
}
//静态函数,创建一个类的函数指针,在注册类的时候要将这个函数地址存储起来;
static IUnkown *create(long iid)
{
IUnkown *result_object = NULL;
NormalObject<Cls> *p = new NormalObject<Cls>();
p->queryObject(iid, &result_object);
return result_object;
}
private:
//私有化构造,不能由外部创建;
NormalObject(const NormalObject &);
//私有化赋值,代表不能由其他赋值;
void operator=(const NormalObject &);
std::atomic<int> _ref_count;
};
下一节我们重点讲如何对类进行注册。