引言:
在大型项目的开发中,随着开发进度的进行,我们经常碰到模块之间耦合度太高的问题:由于开发人员经常要在别的模块中调用自己实现的功能,经常随意在某个函数中随意添加调用代码,造成了被修改的那个函数体过长,逻辑混乱。另一个问题是随意包含头文件:开发人员在开发中经常为了要使用某些类的功能而包含引用类的头文件造成类之间的耦合度太高,被包含类的头文件一处轻微修改经常就会引起整个程序大规模的编译和链接,当编译链接时间达到一定程度时,程序员就会被诱导去做不会导致大规模重编译的改动,而不管改动是否会保持原来的设计。
常规解决方案:
1. 静态类库:设计良好的静态类库能实现功能上的隔离,无法避免类库实现 必须重新编译、链接整个应用程序的问题
2. DLL:但仍有自己的缺点:
a) 函数重名问题:我们通过函数名来调用DLL的函数,在并行开发中容易造成函数重名。
b) 依赖:如果采用常见的隐式连接,那DLL每发行了一个新版本都有必要和应用程序重新链接一次,因为DLL里面函数的地址可能已经发生了改变。
3. COM:DLL的缺点就是COM的优点。但是实际开发中我们会发现COM太复杂了。要使用COM编程,必须要非常熟悉C++中的COM实现细节, 最好之前要有使用和实现COM对象和服务器的经验。开发中而且必须从.idl开始工作才能加入接口属性和方法,对开发和使用都有很高的门槛。
本文的解决方案—简化的组件编程:
实际上我们只是在开发项目,并不需要跨语言编程,也不需要组件的位置透明性。为了项目而引入COM代价往往太过于巨大。然而COM的内部结构对于大多数程序员是无关的。因此有必要对COM进行简化以降低编程门槛。使之更符合常规的变成习惯。所以我们借鉴了COM的优秀思想来构建我们的程序架构,使我们的程序能够像基于COM组件开发那样的灵活,而开发人员又不需要掌握太多的COM知识。下面我们分步介绍我们的实现过程
一、 总体架构:
l应用程序:软件的可执行程序(.exe),通过组件管理器来创建组件,组件创建起来后应用程序直接访问组件,不再通过组件管理器中转。
l 组件管理器:整个框架的核心部分,它本身是一个DLL文件。应用程序通过它来创建、管理所有的相关DLL。作用类似与COM中的COM库。它是应用程序加载的第一个DLL。
l 组件模块:以DLL实现的分解后功能模块。软件的全部功能都在组件中实现,组件与组件之间,组件和应用程序之间并不直接直接耦合,应用程序或一个组件不能直接创建另一个组件的实例,而必须通过组件管理器创建。组件对外并不暴露出类的实现,而仅是通过组件管理器返回接口的指针。
二、 应用程序运行过程:
应用程序的运行序列图:
1. 主程序启动:应用程序在启动阶段调用组件管理器启动应用程序框架。
2. 组件管理器扫描应用程序目录下所有的DLL文件,并动态加载DLL,根据事先约好的注册函数名判断是否是框架组件
3. 查询组件A实现的接口
4. 组件A返回它实现的全部接口ID(CLSID)。
5. 组件管理器把接口ID和对应的组件文件名登记在内部链表中。
6. 同3
7. 同4
8. 同5,
9. 启动过程结束,控制权交还给主程序
10. 业务功能开始:主程序调用组件管理器,启动所有自启动接口
11. 组件管理器查询内部链表,创建自启动接口(组件B实现了自启动接口)
12. 组件B在初始化函数中启动了相关的业务功能。
13. 组件B需要用到接口A,但组件B并不知道谁实现了接口A,于是它调用组件管理器来创建接口A
14. 组件管理器查询链表得知组件A实现了接口A
15. 组件管理器调用组件A的导出函数创建接口A的实例
16. 组件A返回接口A的实例指针
17. 组件管理器将接口A的实例指针传递给接口B
18. 组件B调用接口A来完成某一功能
19. 组件B使用完接口A,直接调用接口A的函数来释放接口A占用的资源
20. 主程序运行结束:调用组件管理器释放所有组件占用资源
21. 组件管理器释放所有自启动接口占用资源。直接调用接口B的函数释放
22. 组件B释放完毕
23. 应用程序退出
三、 应用程序的实现:
应用程序的实现比较简单:仅需在应用程序初始化时加载组件管理器,调用管理器提供的启动框架,启动自启动接口。在退出时调用组件管理器释放所有组件占用的资源即可
四、 组件管理器:
组件管理器是应用程序和组件之间的桥梁。它维护了一张组件接口链表。负责整个框架的启动、组件的创建、还有最后框架资源的释放工作。组件管理器虽然重要,但它的实现却很简单,这里就不在详讲了。
五、 组件:
组件是整个项目的核心,整个应用程序的所有功能都由组件完成。一般而言一个功能点需要由两个组件来完成,一个提供功能服务,一个为自启动组件,调用功能服务。
1. 组件的实现:
l 组件对外只暴露出接口,因此每一个组件至少都由两部分构成,组件接口和组件的实现类。
a) 组件接口:借鉴COM的思想,每一个接口都有唯一的GUID来标示。
组件接口仅定义了一组类的纯虚函数,并不包含实现的任何细节
b) 实现类:是接口的实现。包含全部的实现细节
l 跟COM类似,接口分为单实例和多实例接口。因此需要把创建部分分离出来。创建的代码很相似,所以可以用模板来实现。将公用代码写成静态库,每个组件包含一份可以减少组件的代码编写量。
组件结构图
多实例接口的创建过程:
单实例接口的第一次创建过程与多实例一样。第二次以后的创建为:
结果:
a) 开发独立:每个模块可以单独开发,单独编译,甚至可以单独调试和测试。当所有的组件开发完成后把它们组合在一起就得到了完整的应用系统。当需求发生部分变更时并不需要对所有的组件进行修改,只需修改受影响的组件即可。
b) 修改独立:新增功能只需将实现的DLL放入应用程序目录即可,不需更改原有代码。 除了核心模块,其余功能拼凑可简单通过增删DLL实现
c) 模块独立:在开发过程中强迫程序员和接口而不是具体的类打交道,防止出现耦合性很强的代码。
d) 智能扩展,只需将实现特定接口的COM类(DLL)防入程序所在的目录,程序自动创建它,可以在类的初始化函数内实现程序功能。
e) 可重用性强,因为是针对接口开发,只要符合接口规范就可以重用DLL
下面我们给出了一个按照仿COM架构实现的Demo
1. 单独一个Exe也能运行,虽然只是个空壳子没有功能。
2. 加入ComManager.DLL,于是程序具有了自动扩展功能。
3. 加入了ModuleA.DLL,主界面出现了一个按钮,右机窗口弹出了一个菜单,按钮和菜单均可以响应命令。菜单和按钮的创建和响应命令均在ModuleA.DLL中实现
4. 加入了ModuleB.DLL,主界面出现了另一个按钮,右机窗口弹出的菜单又多了一项,按钮和菜单均可以响应命令。新增的菜单和按钮的创建及响应命令均在ModuleB.DLL中实现
5. 加入Sking.DLL,于是整个程序的界面都具有了肤化效果
6. 加入Log.DLL,于是程序具有了日志功能,可以纪录模块创建的顺序
7. 。。。。。。。。。。。。。。
8. 。。。。。。。。。。
因为程序是基于接口开发的,所以功能的实现和模块的名字无关,和模块加载的顺序也无关(有兴趣可以试一下)----当然ComManager.DLL必须是第一个加载,并且不能更名。