本文是回应 Joshua GPBeta谈中心化插件系统的设计 中的评论。

我先纠正几个误解。

首先,在之前的设计中定义的接口并不是结构体 + 函数指针,而是”接口名 + 函数ID + 函数签名”,
每个接口自公开之时起应该都是固定下来了的,那么,作为接口的公开者,需要提供好”接口名 + API”,这主要是通过一个头文件来提供这个定义的。

例如上文中的 Graphics 接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Graphics v 1.0 接口定义
#define _INTERFACE_NAME "sao.interface.graphics"
#define _VERSION_MAJOR 1
#define _VERSION_MINOR 0
#define _FUNC_CREATE 1
#define _FUNC_RELEASE 2
#define _FUNC_DRAWLINE_ID 3
#define _FUNC_DRAWLINE2_ID 4
#define _FUNC_DRAWRECT_ID 5
#define _FUNC_DRAWRECT2_ID 6
#define _FUNC_DRAWARC_ID 7
#define _FUNC_DRAWARC2_ID 8
struct Point
{
int x;
int y;
};
typedef void* (*_FuncCreate)();
typedef void (*_FuncRelease)(void* obj);
typedef void (*_FuncDrawLine)(void* obj, int x0, int y0, int x1, int y1);
typedef void (*_FuncDrawLine2)(void* obj, const Point& p0, const Point& p1);
typedef void (*_FuncDrawRect)(void* obj, int x0, int y0, int x1, int y1);
typedef void (*_FuncDrawRect2)(void* obj, const Point& p0, const Point& p1);
typedef void (*_FuncDrawArc)(void* obj, int x0, int y0, int r);
typedef void (*_FuncDrawArc2)(void* obj, const Point& p, int r);

接口名称标识以及函数 ID 与函数签名在订立之后就绝对不会发生修改,只允许添加或废弃,这样可以提供足够稳定的二进制兼容性。

而且,需要注意的是,这个头文件主要是给实现者使用的,因为为了提供更加面向对象的 API,接口的定义者一般可以直接使用这个定义来提供一个 C++ 的 wrapper,对于使用者来说,只需要关心这个 wrapper 的接口就可以了,通过 wrapper 提供的接口,也是使用者出错的可能性降到了最低。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct Point
{
float x;
int y;
};
// Graphics 接口定义者提供的 wrapper class
class Graphics
{
public:
static Graphics* create(Plugin* plugin);
void release();
void drawLine(int x0, int y0, int x1, int y1);
void drawLine(float x0, float y0, float x1, float y1);
void drawLine(const Point& p0, const Point& p1);
void drawRect(int x0, int y0, int x1, int y1);
void drawRect(float x0, float y0, float x1, float y1);
void drawRect(const Point& p0, const Point& p1);
void drawArc(int x0, int y0, int r);
void drawArc(float x0, float y0, float r);
void drawArc(const Point& p, int r);
private:
Graphics();
~Graphics();
};

另外,性能在这个设计中绝对不会成为问题,这里的函数 tag 只是用来获取函数指针用的,对于函数指针这类静态型资源,只需要查询一次即可,用结构体存储函数指针是为了使用方便,同时允许批量查询以提升效率,本身是不属于接口定义中的内容的,而对于 IDispatch 这种每次调用都需要查询一次的脑残设计,我只能无语了。

至于接口名称和插件名称使用字符串的设计,是为了保持扩展性的同时提供良好的可读性,而且相较于 GUID 标识方案,字符串标识可以以很自然地方式选取,也不需要什么特殊工具去生成,同时这个标识同时可以使用在其他地方,比如打印所有已注册的接口,可以直接将名称打印出来,而不需要再通过其他方法得到一个可读的名称,至于性能方面的考虑,考虑到这种查询的频度是非常低的,所以使用字符串作为索引是可以接受的。

这种插件体系设计非常的自然,名称标识可读,性能也很不错,而且接口可以以自然的方式演进,通过 version code 来提供兼容性的控制,而且通过 Extension 也可以在 Plugin 级别上提供足够的扩展性,比如提供插件的激活与反激活机制,插件依赖的遍历机制等等,当然,这些机制也可以不通过 Extension 实现,直接使用接口本身来实现,或者两者配合提供更加自由的扩展能力。

update: 2014-12-16 11:58

Joshua GPBeta 的回复:

拿tag设计以及IDispatch比较确实是我的误解了,
但是这种设计本质上还是手工生成虚函数表, 只不过把传统的”函数指针结构体”换个名字叫”pImpl”形式, 解决的是接口名不用带版本号的问题, 但是却增加了上文评论所说的类型检查风险.
个人认为接口带版本号对某个项目并没有根本性的劣化, 相反加入新的风险却是需要再作相关讨论以及大规模的整合测试的.
tag的设计的确几乎不会对性能产生影响, 不过实际上使用COM的接口继承,接口废弃的做法可以实现, 只是接口尾巴会带上版本号.

Joshua GPBeta

对此回复的回应请看 谈中心化插件系统的设计(三)