COM样例(二)
——样例结构设计
本文为《COM样例》系列文章的第二篇,说明样例的结构,解释COM的编程思想,并帮助理解样例代码。
样例目标
欲给一个公司做一个信息管理系统,也就是公司中所有部门的信息可以被输入电脑,并可进行分布式查询,即总经理可随时查询最新的订单情况和出货情况。由于使用COM作为此信息管理系统的基架,可以很容易的解决分布式问题,并且由于COM对安全的包装,使得提供访问控制也变得容易。下面先说明COM提供的编程思想,再以此编程思想设计各接口。
COM编程模型
见过不少这种说法:“COM是更加地面向对象,封装地更彻底”。这里要纠正这种错误的思想,虽然可以说对,但是是错误的应用。这就好像牛刀可以杀鸡,但并不应该被说对。
面向对象编程思想是一种思想,指导如何设计程序架构的。其主打思想就是将被操作数据看成一个个对象。而所谓的对象就是具有状态,并对外提供了接口以暴露其可以提供的服务的逻辑概念。其是状态和功能通过语义的混合体。
其和日常生活很像,比如电视机就既提供了服务——搜台,又提供了状态——哪个频道是哪个台。因此在使用面向对象编程思想时会从对象的概念出发来定义数据结构,这和COM完全不一样。
COM叫做组件对象模型,从名字看其异常明显地表示了最开始引号中的话的正确性,这是个误解。COM最突出的贡献不是组件这个概念,而是接口。
接口表示功能的集合,其不是状态。与面向对象正好相反,其完全不看重对象的实现,甚至淡化对象这个概念,极力强调接口的概念,这在各本COM教科书中表现地很明显——里面第一个讲的就是IUnknown接口,极力强调没有对象指针,只有接口指针。
这看起来有点混乱,如果认为面向对象强调的是状态和功能的混合,COM强调的就是功能集的集合。而类就是只实现了一个接口的COM组件(不包括IUnknown),这从根本上说是COM的退化。因此当设计中的每个COM组件都只实现一个接口时,此时根本不是设计一个COM应用,只是在二进制代码级上应用C++提供的编程思想而已。
由于COM做地并不是那么好,以至于会产生前面所说的误解。其强调功能的概念没有体现出来,而更表现为组件,以至于很容易认为组件是积木,而整个程序就是用不同的组件搭建的房子。这是对象级上的模块化编程。COM不会设计到最后反而跑回老路上去。
搭积木的重点是积木,是以积木来搭建房屋。而COM提供的并不是积木,是积木间衔接的形状,它主张在搭积木前先搭一个架子,不同的积木能放到架子上形成的不同的格子里,架子搭好后再根据架子上形成的格子的形状做积木,最后将积木放到架子上。而不是先做积木,然后根据积木搭房屋(这个比喻并不是非常准确)。
思考这个问题:欲实现任务和任务管理器的功能,设计两个接口ITask和ITaskManager,考虑ITask的功能定义。其代表的是能够作用于任务上的功能,不是任务本身,因此其有如下两个方法:TerminateTask和GetProcessRateOfTask以分别终止任务和得到任务进度。但是很明显,任务是需要启动的。如果按照面向对象的思想,在不考虑设计模式的情况下,很容易想到将任务的发起这个动作作为ITask中的一个方法:StartTask,这样ITask的实现者就是一个完全的任务,如果使用线程进行任务操作,其就连那个线程的操作也一起包装起来,形成一个任务。这不是一个好的设计,ITask是个接口,代表的是功能,不是对象。接口以为实现它的对象就可以照其定义进行操作,因此ITask的实现者是可以被相当于任务一样的操作,而不是任务这个东西。前者具有更好的可扩展性,如可以通过按遥控器来操作东西,但那个东西不一定必须是电视机,而后者就一定要求其是电视机。
因此COM里重点的不是组件,而是接口,这是一种可扩展性相当好的设计思想,可以称做面向接口编程思想。它本身是没有什么缺点的,但其实现方式由于使用对象的概念,则一定和状态关联,这在数据量很大时是不好的。如订单会很容易地就被设计成一个类,然后提供诸如订单结帐、提货等多种服务(即成员函数)。这里的问题就是订单如此之多,如果使用一个数组作为其容器显然性地问题严重,而链表更是应该判死刑。因此这里将订单设计成一个类是很不明智的选择。对于此,应该专门仔细研究如何处理大数据量的技术,并将功能与状态拆开,然后数据变成原材料,而功能变成机器,通过流水线生产以提高效率。即面向对象是个人主义,当数据量大时,就需要分工合作来提高效率了。对于此,Microsoft早已提供了MTS来帮助开发,其中提供的编程思想就是专门针对这种大数据量而设计的,提倡无状态组件,即状态和功能的分离,其对于开发大数据量的应用提供了非常好的支持(关于MTS,可参考我写的另一篇文章《MTS基础教程》)。
样例设计
前面已经说明了COM提供的编程模型,下面就本样例说明如何设计接口。
程序员考虑最多的事应该是如何偷懒,并且美其名曰“代码重用”,但现在又被更好听的名词所替代——“具有可扩展性”。样例是一个公司的信息管理系统,里面人事部门的信息处理和营销部门的将会千差万别,信息有完全不同的流程。因此是肯定需要一个一个编的。但它们能够被称做部门,就一定有共通的地方,这正是程序员最厉害的地方——归纳能力,然后推演出其他东西以达到偷懒的目的。
照前面的说法,由于数据量巨大,因此决定选择数据库而不是建立对象。由于各个部门没有什么同一性(其实还是有的,后叙),最后认为唯一相同的是同属一个公司旗下,故决定提供一个基本框架界面,其提供最基本的如错误处理、日志记录等功能,欲通过在同一个基架下以表现得各部门在同一公司旗下。
基本框架需要提供错误日志的记录以方便系统的维护和查错;需要提供界面框架以容纳不同的部门组件的操作界面,即需要提供菜单命令的提供,也出于Windows界面的想法而提供工具条和快捷键;需要提供任务管理器,因为在海量的数据中查找那么一两条信息不是瞬间的事,因此可能总经理发起了一个人事查找后,又发起一个订单查找或客户查找,但却由于等得不耐烦而终止了前面的人事查找;需要提供数据库系统的相关信息,以使得部门组件可以将数据存储到统一的地方,方便备份等管理。
部门组件需要提供界面以进行信息操作(如录入、查找等),作为Windows界面,常规性地需要提供菜单、工具条的维护性操作(如命令的说明字符串的提供);需要提供任务执行进度,以提高操作者的忍耐限度。
经过上面的决定,基本框架应有4个接口,而部门组件应有2个接口。但请注意,一个接口表示一个功能集合,如果一个组件实现了一个接口就表示其所有功能都实现了,但COM非常可惜地提供了E_NOTIMPL这个错误代码,因此导致了错误的接口设计——里面的方法可以有未实现的。这个错误代码准确的说应该是为将来扩展而预留的,即方法中的某个参数代表功能的种类,如是画圆形还是画矩形,但其可以指定为画椭圆形,这种形状暂时不支持,但相信以后版本将会支持,这才是E_NOTIMPL的真正含义,却被错误的应用了,比如:
上面提到的部门组件应该支持一个接口,其提供包含部门组件界面、菜单、工具条和快捷键的提供。完全有可能一个部门组件不使用工具条进行操作,完全使用一个对话视搞定一切,那么当可怜的基本框架错误地以为其需要工具条的空闲处理而调用了相应方法时却得到一个E_NOTIMPL时,应该怎样?因此应该将一定会同时存在的功能归为一个接口,因此出于上面的考虑,应该再提供三个接口:快捷键处理、菜单处理、工具条处理。由于快捷键没有处理,只是获得即可,不像菜单还需提供菜单状态的处理等操作,所以无须快捷键处理的接口,因此部门组件应该具有4个接口,其中三个是可选的。
上面的接口分工显得有些牵强,不过这只是粒度粗细的问题。如果愿意粗粒度,也可以说成基本框架只需一个接口,如果要细粒度,也可再定个工具条处理接口和菜单处理接口,这里就是见仁见智的地方了。但还是建议至少要保证接口中的方法如果实现一个,则其他的逻辑上也都需实现。
最后其IDL定义文件如下:
import "oaidl.idl";
import "ocidl.idl";
// 基本框架实现IModuleSite,其提供基本的操作
[
object,
uuid(1A201ABA-A669-4ac7-9DF8-2DA772E927FC),
pointer_default(unique)
]
interface IModuleSite : IUnknown
{
// 供部门组件改变当前显示模块,如点击了营销模块中的订单查找结果中的
// 办理人字段后自动跳转到人事模块中显示办理人的相关信息
HRESULT ChangeModule( [in] REFCLSID clsid, // 模块的CLSID
// 模块名字,仅用于提示
[in, string] WCHAR *pModuleName,
// 模块命令,指明欲让模块执行的命令,由模块解释
[in] ULONG command,
[in] ULONG param ); // 模块命令的相关参数
HRESULT GetFrameWindow( [out] HWND *pHwnd ); // 返回主框架窗口
};
// 基本框架实现IErrorReport,其提供报告错误的功能
[
object,
uuid(1A201ABA-A669-4ac7-9DF9-2DA772E927FC),
pointer_default(unique)
]
interface IErrorReport: IUnknown
{
// 报告温和型错误,相当于警告
// fileName代表源代码文件的名字,row代表错误所在行
HRESULT ReportSoftError( [in, string] WCHAR *fileName,
[in] ULONG row,
[in, string] WCHAR *errorString );
// 报告暴力型错误,相当于错误
HRESULT ReportHardError( [in, string] WCHAR *fileName,
[in] ULONG row,
[in, string] WCHAR *errorString );
}
// 基本框架实现ICompanyInfo,其提供数据库服务器信息
[
object,
uuid(1A201ABA-A669-4ac7-9DFA-2DA772E927FC),
pointer_default(unique)
]
interface ICompanyInfo: IUnknown
{
// 返回数据库服务器的相关信息,主机IP、服务器名字及密码
HRESULT GetDataServerInfo( [in, string] WCHAR *loaction,
[in, string] WCHAR *server,
[in, string] WCHAR *password );
}
// 基本框架实现ITaskManager,其提供任务的操作
interface ITask;
[
object,
uuid(1A201ABA-A669-4ac7-9DFB-2DA772E927FC),
pointer_default(unique)
]
interface ITaskManager: IUnknown
{
// 添加任务
HRESULT AddTask( [in, string] WCHAR *taskString, // 任务说明字符串
[in] ITask *pTask, // 任务的指针
// 返回标识一个任务的cookie
[out] DWORD* pCookie );
};
// 基本框架实现ITaskNotify,其提供任务的通知
[
object,
uuid(1A201ABA-A669-4ac7-9DFC-2DA772E927FC),
pointer_default(unique)
]
interface ITaskNotify: IUnknown
{
// 通知指定任务的进度已经变化
HRESULT ProcessRateChange( [in] DWORD cookie );
// 通知任务已经结束
HRESULT TaskOver( [in] DWORD cookie );
};
// 部门组件必须实现IModule,其提供模块的操作
[
object,
uuid(1A201ABA-A669-4ac7-9DFD-2DA772E927FC),
pointer_default(unique)
]
interface IModule: IUnknown
{
// 初始化模块,nID为模块窗口的子窗口ID
HRESULT InitialModule( [in] IModuleSite *pSite, [in] UINT nID );
// 返回模块的图标
HRESULT GetIcon( [out] HICON *pHicon );
// 返回模块的名字
HRESULT GetName( [out, string] WCHAR **pName );
};
// 部门组件不一定实现IModuleCommand,其提供执行模块所特有的命令
[
object,
uuid(1A201ABA-A669-4ac7-9DFE-2DA772E927FC),
pointer_default(unique)
]
interface IModuleCommand: IUnknown
{
HRESULT DoCommand( [in] ULONG command, [in] DWORD param );
};
// 部门组件不一定实现IModuleNotify,其对模块提供一个通知途径
[
object,
uuid(1A201ABA-A669-4ac7-9DFF-2DA772E927FC),
pointer_default(unique)
]
interface IModuleNotify: IUnknown
{
HRESULT OnActivate(); // 模块切换时被激活
HRESULT OnDeActivate(); // 模块切换时取消激活
};
// 部门组件必须实现IModuleUI,其提供模块界面的相关操作
[
object,
uuid(1A201ABA-A669-4ac7-9E00-2DA772E927FC),
pointer_default(unique)
]
interface IModuleUI: IUnknown
{
// 返回模块的主要窗口
HRESULT GetMainWindow( [out] HWND *pHwnd );
// 翻译快捷键
HRESULT TranslateAccelerator( [in] MSG *pMsg );
};
// 部门组件不一定实现IMenuUdpate,其提供模块界面中菜单的相关操作
[
object,
uuid(1A201ABA-A669-4ac7-9E01-2DA772E927FC),
pointer_default(unique)
]
interface IMenuUpdate: IUnknown
{
HRESULT GetMenu( [out] HMENU *pHmenu );
HRESULT GetMenuItemString( [in] ULONG nID,
[out, string] WCHAR **pString );
};
// 当部门组件创建了一个任务时,任务对象必须实现ITask以进行相应的任务管理
[
object,
uuid(1A201ABA-A669-4ac7-9E02-2DA772E927FC),
pointer_default(unique)
]
interface ITask: IUnknown
{
// 返回任务的进度
HRESULT GetProcessRateOfTask( [out] float *pRate );
HRESULT TerminateTask(); // 终止任务
// 将任务和任务管理器绑定起来
HRESULT SetTaskSite( [in] ITaskManager *pManager, [in] DWORD cookie );
};
[
uuid(1A201ABA-A669-4ac7-9D00-2DA772E927FC),
version(1.0),
helpstring("ExampleBase 1.0 TypeLib")
]
library ExampleBaseLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
interface IModuleSite;
interface IErrorReport;
interface ICompanyInfo;
interface ITaskManager;
interface IModule;
interface IModuleCommand;
interface IModuleNotify;
interface IModuleUI;
interface IMenuUpdate;
interface ITask;
interface ITaskNotify;
};
上面的设计有个很明显的问题就是并没有体现组件的特性,只是很简单的部门组件和基本框架的组合,部门组件不能再有什么其他作为,是一种变相的DLL技术。这是样例的目标及特点(各部门完全不一样的信息处理方式)决定的,就是一个插件接口。基本框架相当于一个播放器,而部门组件相当于一种音效处理插件。由于这只是个简单的例子,无法表现出COM组件特性的优点,但就此样例给出线程模型的例子已经是足够了。
如果每个部门组件都只是信息录入、信息查询和信息管理(忽略其业务流程,如订单需要和出货联系起来),则可以使用另一种功能分割方式,即信息表现的接口、录入信息的接口、查询信息的接口和管理信息的接口(甚至还可以抽象出业务进而形成业务接口),这种方案将体现出组件的概念,但复杂程度亦增加了不少,因为其灵活性大大高于前一种方案。
由于添加工具条的支持需要更多的代码,并且对本样例没有什么意义,故本样例中没有提供对工具条的支持。
作为一种习惯,将工程中所有的接口定义在一个.idl文件中,然后再专门定一个项目生成其代理/占位组件,并导出IID等这类全局变量以供以后的使用,并且可以将类型信息一起加入其中,以减少最终完成中的文件数量。对于此,我已经在样例的解决方案中如此做了,源代码请在《COM样例(四)》中下载。