最近公司准备开发一个新产品,需要重新设计一套新的框架,但是就这框架中各模块的通信方式,大家产生了争论,主要集中在各模块的交互方式是消息耦合还是接口耦合。
需求大概这样,我们需要封装一套客户端SDK, 暴露一系列API给外部用,而这套SDK内部会有很多模块组成,这些模块之间相互会有交互。
第一种设计是基于接口耦合,框架如下:
这种接口方式的设计要点是:
a. 各模块以类似COM组件的方式封装和暴露接口,也就是说模块会以接口的形式暴露接口,并且以Sink的方式通知外部事件。比如模块A的接口如下
c. 各模块间的交互全都通过直接调用其他模块的接口或是订阅该模块的Sink来实现。
第二种设计是基于消息耦合,框架如下:
这种消息方式的设计要点是:
a. 各个模块只有一个消息处理接口。
c. 需要调用某个模块接口 或是 触发某个事件时,都是通过向消息总线发送消息的方式, 然后由订阅消息的模块执行。
上面2种架构的设计方式各有优劣,下面我们来简单比较一下:
(1)耦合性: 尽管基于接口的耦合已经降低了耦合性,但是相比消息来说,显然是消息方式耦合性更弱。
(2)可扩展性: 某个模块新增加一个接口, 接口方式需要新加接口函数,而消息方式只需要新加一个消息类型。即使新增加一个模块,消息方式只是新增加几个消息处理类型,非常方便。所以可扩展性来说,显然也是消息方式占优。
(3)性能: 接口方式是直接调用,可是消息方式需要经过消息总线过滤分发, 显然性能上接口方式更高。
(4)编码安全性,接口方式是强类型,接口一修改,编译时就能很快发现问题;消息方式却是弱类型,消息修改后,有可能要到运行时才能发现问题, 另外很多消息内容要做强制了类型转换才能使用。
(5)文档要求: 显然接口方式相对比较清晰,消息的话每个消息都要详细定义,并且严格按照该定义执行。
(6)可调试性: 显然接口方式要方便些,消息很可能不小心就会引起混乱。
(7)监控过滤方便性:消息方式走同一总线,可以很方便的增加过滤和监控功能, 接口方式则因为各个模块interface和Sink各不相同,增加这些功能没那么方便。
(8)跨线程或是跨进程,甚至跨机器调用:显然接口方式基本做不到,消息方式的话只要修改总线就可以做到。
(9)异步支持: 消息方式可以很方便的支持异步,接口方式则做不到。
需求大概这样,我们需要封装一套客户端SDK, 暴露一系列API给外部用,而这套SDK内部会有很多模块组成,这些模块之间相互会有交互。
第一种设计是基于接口耦合,框架如下:
![](https://i-blog.csdnimg.cn/blog_migrate/716fa31cc68f8792572cabc284595b36.png)
这种接口方式的设计要点是:
a. 各模块以类似COM组件的方式封装和暴露接口,也就是说模块会以接口的形式暴露接口,并且以Sink的方式通知外部事件。比如模块A的接口如下
class
IA
{
public :
virtual void fun1() = 0;
virtual void fun2() = 0;
.
virtual void int AdviseSink(IASink* pSink) = 0;
virtual void bool UnAdviseSink( int nCooki) = 0;
}
class IASink
{
public :
virtual void Event1() = 0;
virtual void Event2() = 0;
.
}
b. 有一个统一的模块管理平台来管理所有模块,可以通过该平台查询和加载需要的模块,然后后得到相应的接口指针进行操作。
{
public :
virtual void fun1() = 0;
virtual void fun2() = 0;
![](https://i-blog.csdnimg.cn/blog_migrate/918e8df969f9f8c8d002f25cda86cade.gif)
virtual void int AdviseSink(IASink* pSink) = 0;
virtual void bool UnAdviseSink( int nCooki) = 0;
}
class IASink
{
public :
virtual void Event1() = 0;
virtual void Event2() = 0;
![](https://i-blog.csdnimg.cn/blog_migrate/918e8df969f9f8c8d002f25cda86cade.gif)
}
c. 各模块间的交互全都通过直接调用其他模块的接口或是订阅该模块的Sink来实现。
第二种设计是基于消息耦合,框架如下:
![](https://i-blog.csdnimg.cn/blog_migrate/95a1627c78a6b0fce2ab41602b5f2093.png)
这种消息方式的设计要点是:
a. 各个模块只有一个消息处理接口。
class
IMessageHandler
{
public :
virtual void ProcessMessage(Message& msg) = 0;
};
b. 中间的消息总线提供消息的订阅和分发,各个模块会向消息总线注册自己感兴趣的消息。
{
public :
virtual void ProcessMessage(Message& msg) = 0;
};
c. 需要调用某个模块接口 或是 触发某个事件时,都是通过向消息总线发送消息的方式, 然后由订阅消息的模块执行。
上面2种架构的设计方式各有优劣,下面我们来简单比较一下:
(1)耦合性: 尽管基于接口的耦合已经降低了耦合性,但是相比消息来说,显然是消息方式耦合性更弱。
(2)可扩展性: 某个模块新增加一个接口, 接口方式需要新加接口函数,而消息方式只需要新加一个消息类型。即使新增加一个模块,消息方式只是新增加几个消息处理类型,非常方便。所以可扩展性来说,显然也是消息方式占优。
(3)性能: 接口方式是直接调用,可是消息方式需要经过消息总线过滤分发, 显然性能上接口方式更高。
(4)编码安全性,接口方式是强类型,接口一修改,编译时就能很快发现问题;消息方式却是弱类型,消息修改后,有可能要到运行时才能发现问题, 另外很多消息内容要做强制了类型转换才能使用。
(5)文档要求: 显然接口方式相对比较清晰,消息的话每个消息都要详细定义,并且严格按照该定义执行。
(6)可调试性: 显然接口方式要方便些,消息很可能不小心就会引起混乱。
(7)监控过滤方便性:消息方式走同一总线,可以很方便的增加过滤和监控功能, 接口方式则因为各个模块interface和Sink各不相同,增加这些功能没那么方便。
(8)跨线程或是跨进程,甚至跨机器调用:显然接口方式基本做不到,消息方式的话只要修改总线就可以做到。
(9)异步支持: 消息方式可以很方便的支持异步,接口方式则做不到。
经过上面的比较, 我们可以得出一些结论:
消息方式的强项是耦合性和扩展性,以及监控的方便性,个人感觉比较适合于Server端的大规模应用。
接口方式的强项是性能高效以及开发的方便性, 比较适用于同一进程内客户端的小规模应用。
但是大部分时候, 对于架构师或是公司领导,他们会更关注可耦合性和可扩展性,所以他们会倾向于选择消息方式,尽管有时可能不是那么适用。
另外,个人觉得编译时静态类型检测是C++的优势,能让我们高效而可靠的开发程序,我们不应该放弃这些优势而去把C++当作弱类型的动态语言来使用,我在 软命令接口的适用场合 一文也有相关描述。
最后,对于该框架的设计,其实我个人倾向于上面2种方式的结合, 即各个模块的入接口(调用接口)走接口方式,而各模块内部触发的事件走消息总线的方式,虽然没人采用这种方式,还是在这里记录一下。
一个多月了,消息还是接口,领导们老大们关于走何种架构还在争论中, 我等小兵就默默等待吧 ...
呵呵,不知道各位看客会作何选择?