com原理

 一、COM編程思想--面向組件編程思想(COP)

  衆所周知,由CC++,實現了由面向過程編程到面向對象編程的過渡。而COM的出現,又引出了面向組件的思想。其實,面向組件思想是面向對象思想的一種延伸和擴展。因此,就讓我們先來回憶一下面向對象的思想吧。

  面向對象思想是將所有的操作以及所操作的對象都進行歸類(class實現),而它的目標是要儘量提高代碼的可重用性(這也是面向對象相比面向過程最大的優點之一)。比如,有兩個程序AB都需要對class C的對象進行操作,那麼class C的代碼就可以重用了(AB都可以使class C的代碼)。但是,對於這一點,面向對象做得並不夠好。還是舉剛纔的例子,程序AB都要對class C的對象進行操作,那麼,程序AB的編程人員都必須將class C的代碼拷貝過來,然後重新編譯一次,這將是多麼麻煩的事!況且,如果class C的代碼沒有公開,那這種重用就根本不可能實現了(除非程序AB的編程人員和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的函數,這個函數可以被外部的客戶調用。它返回一個接口的指針,當客戶調用這個函數後就能夠獲得指向組件實體的接口指針了。它的實現也很簡單:

//在實現組件ComponentBdll裏:
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、實現了組件ComponentBComponentDll.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;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章