COM原理


COM--Component Object Model,即组件对象模型,它是微软提出的一套开发软件的方法与规范。它也代表了一种软件开发思想,那就是面向组件编程的思想。

  一、COM编程思想--面向组件编程思想(COP)

  众所周知,由C到C++,实现了由面向过程编程到面向对象编程的过渡。而COM的出现,又引出了面向组件的思想。其实,面向组件思想是面向对象思想的一种延伸和扩展。因此,就让我们先来回忆一下面向对象的思想吧。

  面向对象思想是将所有的操作以及所操作的对象都进行归类(由class实现),而它的目标是要尽量提高代码的可重用性(这也是面向对象相比面向过程最大的优点之一)。比如,有两个程序A和B都需要对class C的对象进行操作,那么class C的代码就可以重用了(即A和B都可以使用class C的代码)。但是,对于这一点,面向对象做得并不够好。还是举刚才的例子,程序A和B都要对class C的对象进行操作,那么,程序A和B的编程人员都必须将class C的代码拷贝过来,然后重新编译一次,这将是多么麻烦的事!况且,如果class C的代码没有公开,那这种重用就根本不可能实现了(除非程序A和B的编程人员和class C的编程人员是同一个人或者团队,但这样局限性就相当大了)。

  由于面向对象的这些局限性,很多程序员就会想,如果我们编程需要重用别人的成果时,不需要重新编译别人的代码那就好了。换句话说,我们要达到的目标是,直接重用别人的成果而不是重用别人的代码。这样说也许很抽象,举个例子大家就会比较明白。比如将class C的代码编译生成一个dll,那么当其他程序员想要重用class C时,就只需要在自己的程序中加载这个dll而不需要重新编译class C的代码了(这也就是组件必须要能动态链接的原因)。正是这种思路引出了面向组件的编程思想。

  下面,我就简单介绍一下面向组件的思想。在以前,应用程序总是被编写成一个单独的模块,就是说一个应用程序就是一个单独的二进制文件。后来在引入了面向组件的编程思想后,原本单个的应用程序文件被分隔成多个模块来分别编写,每个模块具有一定的独立性,也应具有一定的与本应用程序的无关性。一般来说,这种模块的划分是以功能作为标准的。比如,一个网上办公管理系统,从功能上说它需要包含网络通信、数据库操作等部分,我们就可以将网络通信和数据库操作的部分分别提出来做成两个独立的模块。那么,原本单个的应用程序就分隔成了三个模块:主控模块、通信模块和数据库模块。而这里的通信模块和数据库模块还可以做得使其具有一定的通用性,那么其他的应用程序也就可以利用这些模块了。这样做的好处有很多,比如当对软件进行升级的时候,只要对需要改动的模块进行升级,然后用重新生成的一个新模块来替换掉原来的旧模块(但必须保持接口不变),而其他的模块可以完全保持不变。这样,软件升级就变得更加方便,工作量也更小。

  说了这么多,总结一下:面向组件编程思想,归结起来就是四个字:模块分隔。这里的“分隔”有两层含义,第一就是要“分”,也就是要将应用程序(尤其是大型软件)按功能划分成多个模块;第二就是要“隔”,也就是每一个模块要有相当程度的独立性,要尽量与其他模块“隔”开。这四个字是面向组件编程思想的精华所在,也是COM的精华所在!理解了这四个字,也就真正理解了面向组件编程的思想。(这里说一点题外话,COM其实是一套规范或者说一套标准,但是在我看来,COM的核心还在于它的思想,也就是面向组件编程思想。标准谁都能定,但是思想只有一个!)


  二、COM的优点

  COM的优点也就是面向组件编程思想的优点。而面向组件编程思想有很多的优点,上面所说的便于软件升级只是其中之一。对于它的优点,我总结了一下,有下面几条:

  1、便于重用,使软件开发更快捷

  2、便于软件升级

  3、便于软件开发的分工协作

  4、便于用户定制自己的应用

  以上几点,第一和第二点都不用再多说了,前面讲面向组件编程思想的部分里面已经充分展示出了这两点优点。在这里我解释一下第三和第四点。

  如今的很多大型软件,都不可能由某一个人单独开发,甚至不会由某一个公司去单独开发。这是因为现在的很多大型软件,综合性太强,涉及的面也太广。而一个人的精力是有限的,不可能学会这么多方面的知识,也不可能掌握到这么多方面的编程技术,即使有可能,这样做的效率也是很低下的。所以,通常的情况是分工协作。仍以前面提到的网上办公管理系统为例,这个系统分为了三个模块:主控模块、通信模块和数据库模块。由于这三个模块具有相当的独立性,那么就可以将现有的所有开发人员分为三组,每一组负责一个模块。而这三组之间,只需要商量好相互间的接口就可以了。这样,对于每一个开发人员来说,就不需要掌握所有的编程技术,甚至不需要了解其他模块的具体实现,而软件仍然能有效的开发成功。这就是所谓的便于软件开发的分工协作了。

   除此之外,如果一个大型的软件希望允许用户在一定程度上定制自己的应用,那么COM也是最好的选择。比方说一个软件由两个模块组成,模块A和模块B,现在软件的开发商希望给予用户一定的灵活性,希望可以允许用户自己定制模块B来实现自己特定的应用,那么就只需要公开模块B的所有接口;而用户自己编程实现模块B时也只需要实现了所有的这些接口就行了。当然,这里面还有很多问题,比如COM组件的注册,这涉及到COM标准的一些细节,在这里不作讨论。

  三、COM中的几个重要概念

  1、组件:

  其实只要你仔细阅读了前面的部分,组件的概念应该已经很清楚了。这里所说的组件,就是前面反复在讨论的所谓“模块”。现在我只想强调一下组件需要满足的一些条件。首先是封装性,组件必须向外部隐藏其内部的实现细节,使从外部所能看到的只是接口。然后是组件必须能动态链接到一起,而不必像面向对象中的class一样必须重新编译。

  2、接口:

    由于组件向外部隐藏了其内部的细节,因此客户要使用组件时就必须通过一定的机制,也就是说要通过一定的方法来实现客户与组件之间的通信,这就需要接口。所谓接口就是组件对外暴露的、向外部客户提供服务的“连接点”。外部的客户见不到组件内部的细节,它所能看到的只是接口,客户也是通过接口来获取组件提供的服务。这有点像OSI网络协议分层模型,每一层就像一个组件,它内部的实现细节对于其他层是不可见的;而每一层通过“服务接入点”向其上层提供服务,这就像这里所说的接口。一般来说,接口总是固定的,也是公开的。组件的开发人员要实现这些接口,而客户则通过接口获得服务。正是接口的这种固定和公开,才使得组件和客户能够在不了解对方的情况下达成一致。

  3、客户:

  这里所说的客户不是指使用软件的用户,而是指要使用某一个组件的程序或模块。也就是说,这里的客户是相对组件来说的。

  四、COM的实现原理与雏形模拟

  COM编程的一个重要特点就是要模块化,说得具体一些,就是要将客户和组件分隔开来,而客户和组件之间又是通过接口来通信的。下面,我就介绍一下COM是怎样将客户与组件分隔开来,又是怎样利用接口来实现客户与组件间的通信的。

  首先我要讲讲接口。COM中的接口实际上是一个函数地址表,当组件实现了这个接口后,这个函数地址表中就填满了组件所实现的那些接口函数的地址。而客户也就是通过这个函数地址表获得组件中那些接口函数的指针,从而获得组件所提供的服务的。从某种意义上说,我们可以把接口理解为c++中的虚拟基类;或者说,在c++中可以用虚拟基类来实现接口!这是因为COM中规定的接口的存储结构,和c++中的虚拟基类在内存中的结构是一致的。其存储结构如下图:   

  

                                         虚函数表

               vtbl指针------>Fun1()指针-------->

                                       Fun2()指针-------->

                                       Fun3()指针-------->

                                       …………

  
  Vtbl指针指向一个虚函数表,而这个虚函数表的表项就是指向这些虚函数的指针。

  接口有了,那么组件又是怎样实现接口的呢?实际上,如果用虚拟基类来实现接口,那么组件就是对这个虚拟基类的继承。大家知道,当某个类继承于一个虚拟基类的时候,它就要实现这个虚拟基类里声明的虚函数,这就正好与组件实现接口这一点相吻合。举一个例子来说明,有一个接口InterfaceA,组件ComponentB要实现这个接口,那么就可以这样用c++语言来描述:

//接口:
class InterfaceA
{
  virtual void Fun1()=0;
  virtual void Fun2()=0;
};

//实现了接口InterfaceA的组件:
class ComponentB: public InterfaceA
{
  virtual void Fun1()
  {
     printf("Fun1"n");
  }
  virtual void Fun2()
  {
     printf("Fun2"n");
  }

};

  而客户只需要得到一个指向ComponentB实体的InterfaceA指针就可以获得ComponentB组件的服务了:

//使用了组件ComponentB的客户:
……
ComponentB CB;
InterfaceA *pIA=&CB;  //获得指向ComponentB实体的InterfaceA指针,以下客户就可以只通过接口来获取组件的服务
pIA->Fun1();
pIA->Fun2();
……


  但是我们注意到,这样做组件ComponentB和客户还是没有被完全分隔开。因为在客户代码里需要创建ComponentB实体,这对于只能看到接口而对组件一无所知的客户来说,是不可以接受的(比如客户不会知道组件的类名叫ComponentB)。解决这个问题的方法是在实现组件的动态链接文件(比如dll文件)里创建组件的实体,而不是在客户代码里创建组件实体。通常组件都是以dll的形式出现的,而在实现组件的dll里都会实现一个叫CreateInstance的函数,这个函数可以被外部的客户调用。它返回一个接口的指针,当客户调用这个函数后就能够获得指向组件实体的接口指针了。它的实现也很简单:

//在实现组件ComponentB的dll里:
InterfaceA *CreateInstance()
{
   ComponentB CB;
   InterfaceA *pIA=&CB;
   return pIA;
}

(注:在 DirectX 编程时,很多时候都会用到CreateXXX之类的函数,道理就是这样。)


  当然,真正的CreateInstance函数没有这么简单,我上面的代码只是一个简单的模拟。有个CreateInstance函数之后,客户代码就变成了:

//使用了组件ComponentB的客户:
……
InterfaceA *pIA=CreateInstance();  //获得指向ComponentB实体的InterfaceA指针,以下客户就可以只通过接口来获取组件的服务
pIA->Fun1();
pIA->Fun2();
……


  这样,组件和客户就完全被分隔开了,而连接它们的只有接口以及一个CreateInstance的函数。

  以上就是COM的基本原理了。当然,我前面也说了,COM其实是一套规范,它定义了很多标准,比如COM规定每个接口都必须继承于一个叫IUnknown的接口。我这里基本上没有提及它的这些标准,只是希望能通过对它进行一个简单的模拟来说清楚它的实现原理。下面就给出我模拟COM机制实现的一套COM的雏形,希望能对大家理解COM有帮助。


  1、实现了组件ComponentB的ComponentDll.dll:

//Interface.h
//接口
class InterfaceA
{
public:
  virtual void Fun1()=0;
  virtual void Fun2()=0;
};

//Component.h
//组件(实现了接口InterfaceA)
class ComponentB: public InterfaceA
{
public:
 virtual void Fun1()
 {
  printf("Fun1"n");
 }
 virtual void Fun2()
 {
  printf("Fun2"n");
 }

};

//ComponentDll.cpp
//CreateInstance函数
ComponentB instance;
extern "C" _declspec(dllexport) InterfaceA *CreateInstance()
{
 InterfaceA *pIA=&instance;
 return pIA;
}


  2、客户Client.exe:

//Client.cpp
#include "Interface.h"
#pragma comment(lib,"ComponentDll")
int main(int argc, char* argv[])
{
 InterfaceA *pIA=0;
 pIA=CreateInstance();
 if(pIA!=0)
  pIA->Fun1();
 return 0;
}

COM