前面已经说明了COM提供的编程(www.cppentry.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), ] 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等这类全局变量以供以后的使用,并且可以将类型信息一起加入其中,以减少最终完成中的文件数量。