COM與多型的介面(使用Delphi)

//作者: 酷小毅 (酷小毅的程式試鍊場)

//2002/05/03 於永和

COM與多型的介面

1.為何談介面的多型

到現在相信大家對於物件導向OOP的觀念都非常清楚了,而這裡我要在一次強調的是關於COM對於物件導向的支援以及觀念與傳統的物件導向有些不同,在前面筆者提過傳統Class的實作繼承中SubClassReferenceSuperClass上所必須參考到的資訊,而這些資訊如果修改的話勢必影響的是所有的子類別,這是非常嚴重的,在近代的OOP觀念中強調介面與實作分開,介面與實作分開的好處這裡就不在重複,有興趣的請參考筆者的Delphi COM/DCOM分散式多層應用文章。我們知道COM的介面是可以繼承的,不過要注意的是COM 並沒有完全的支援繼承,也就是說介面的繼承並不會繼承該介面後端的實作部分,也就是其行為模式,不過父物件(SuperClass, 這裡也做父系的COM Object) 可以用本身的Reference和型別相容的SubClass溝通,這也就是我們今天要討論的重點。

圖(一). SuperClass可以和跟他型別相容的SubClass (CApp1, CApp2等)溝通,因為他們都是和CPAutoInterface型別相容的。

 image

2.透過(介面)的實作繼承來達到介面多型

我們今天要探討的是延伸於COM當中的OOP的多型觀念,目前為止相信大家已經了解實作繼承如何影響及重複使用類別當中的方法及實作,這對減少重複性程式碼不僅有高度的Reuse特性也提供了更高的維護性,而多型就是由OOP的實作繼承所演伸出的一個特色,這可以說是物件導向當中最重要的觀念了,我們知道COM強調介面與實作分開,我們可以在一些SubClass繼承某個介面來達到介面的多型(關於傳統物件導向的多型與COM的多型之差異性有興趣可參考筆者的"Delphi如何實作繼承一個介面"文章,這裡不在重複說明),這若應用在IDispatch介面所支援的Automation技術上即則可達到跨行程呼叫不同應用程式的目的(這我會在下面第4個章節說明),但介面繼承它不像傳統的實作繼承可以直接延伸存取SuperClass的細節部分及存取SuperClass的屬性與方法,看起來好像傳統的實作繼承比較強大,但是這使的SubClass完全依賴SuperClass的特性,使的類別失去一些封裝的特性,當一個程式愈來愈大時就會限制該程式的延展性,而透過介面繼承則可確保封裝的特性,只是當你繼承一個介面你必須滿足這個介面當中所有具有的印數及物件等,也就是說你續須在撰寫額外的程式碼,不過這對大型的應用程式而延會有較佳的延展性。

3.透過IDispatch介面支援的Automation技術

AutomationCOM加上IDispatch介面的特殊支援,目前也為COM提供的一種技術,照微軟的說法Automation只是ActiveX當中的一種應用而已,Automation是以應用程式為導向,COM則是以元件為導向的,而當初Automation之所以會出現是因為COMvTable Binding特性,只有一些支援vTable Binding特性的編譯語言如C, C++, VB, Delphi等....可再編譯時期連結COM物件,但還是會有一些程式語言不支援vTable, TypeLibrary,如一些非編譯式語言腳本語言VBScript, JavaScript, ASP等,IDispacth提供執行時期連結(Runtime Binding),又稱做晚期連結,這些特性連結全依賴IDispatch所提供的四個Method如下圖:

圖(二):

 image

當一個Client在呼叫Automation Object時他會先呼叫GetIDsOfNames()詢問該物件是否支援某個Method成員函式,如果Object不支援該Method成員函式,GetIDsOfNames()方法的呼叫就會失敗,動作就無法繼續下去,若GetIDsOfNames()呼叫成功則會傳回一個叫做DISPID的函式識別碼(DISPID為COM的函式庫產生給讓Object能夠識別自己Method的一組2Byte的數字),接著傳入DISPID給Invoke()便能夠呼叫正確的Method,也就是說透過IDispatch的支援能讓一些較不複雜的一些COM Client以比較高的彈性呼叫COM Object,不過當然比較高的彈性是要付出代價的==>也就是晚期連結是很沒效率的,因為他每次的Invoke()呼叫都會造成COM Server與Client多出兩次的往返,由於應用上的方便與簡單,在微軟系統還是到處看的見,如用ActiveX技術將OLE DB包起來的ADO,以及可以提供Service給其他程式的Office系列套裝軟體Word, Excel, Access, Outlook等...像這類的應用程式又稱做OLE Server

4.透過介面的多型來達到呼叫不同應用程式 (Automation的應用)

聽我說了這麼多的理論不知讀者會不會有些無聊,忍耐點,現在要討論的則是Automation的應用,Automation可說是上面所有技術所延伸出來的一種技術,只是Automation是以應用程式為導向,提供介面服務給其他的應用程式,所以通常我們我見到的Automation OLE Server通常都是執行檔,當然你也可以說你的ActiveX DLL提供Automation Service(糟糕名詞越來越多~),但是各位不要忘了,COM的DLL的CoClass的公寓執行緒只是提供有建立產生物件,DLL本身並無法產生Process,執行檔才可已。那我怎麼要怎麼透過這種方式呼叫其他的應用程式呢,其實如果你有看清楚我前面說過的你應該已經知道了,我們可以在另一個應用程式延伸繼承上一個應用程式的某個介面,接著透過介面轉型,IDispacth會查詢Object是否有支援需要的介面,介面是否相容等, 當然我們是延伸界面與父介面當然是相同的,這樣的情況下就達到執行時期透過介面轉型呼叫其他應用程式的目的。

(1).基礎介面範例應用程式

現在筆者Delphi6撰寫一個簡單的範例(此範例也可用Delphi5開啟,但要注意一些新舊Project的問題以及一定要重新Build過),建立的過程當中我們會先建立一個CPAutoInterface的Automation執行檔,將此Automation Object命名為CPAutoInterface,利用TypeLibrary編輯器在IAutoInterface建立一個SendMessage方法,並在方法中傳入兩個參數,如圖(三):

圖(三).在TypeLibrary編輯器中建立兩個參數

 image

然後Delphi會為們產生介面宣告如下:

interface
  uses ComObj, ActiveX, CPAutoInterface_TLB, StdVcl, Unit1;
  type TAutoInterface = class(TAutoObject, IAutoInterface)
    private iCount: Integer;
    sMessage: WideString;
  protected
   procedure SendMessage(const Value, SvrAppName: WideString); safecall;
//Delphi幫我們宣告的部份
    { Protected declarations }
  end;

由於這是一個空的介面所以我們不加入任何的實做部分。

(2).建立App1 and App2 應用程式

接著一樣再開啟一份新的Automation Object Project專案,並將Name設為App1,然後引用CPAutoInterface的TypeLibrary,同樣的記得在TypeLibrary編輯器的Uses也必須引用,即Implements IAutoInterface介面,較細部的部分讀者可查看我的上一篇文章,這裡不再重複說明,接著在TApp1.SendMessage方法中建立如下程式碼:

procedure TApp1.SendMessage(const Value, SvrAppName: WideString);
begin
Form1.labMessage.Caption := Value; Form1.GroupBox1.Caption := '接受到來至' + SvrAppName + '的訊息!!.';
ClientClassName := SvrAppName;
//當此Method被呼叫時, 即填入Server的屬性值
end;

圖(四),App1執行畫面,圖中Label元件會顯示所接收的訊息:

image

App2與App1建立方法完全相同,只是Application Name不同,建立兩個不同名稱的應用程式的目的就是為了展示介面多型的應用,實際上的應用可能是兩個功能已經不同的應用程式透過這種方式呼叫另一個Automation來完成另一件工作。

(3).撰寫CPAutoInterface程式碼

再來要開始撰寫client程式的主要程式碼, 因為它必須去建立App1及App2的OLE物件,並呼叫其實作的IAutoInterface介面,圖五為client程式也就是CPAutoInterface程式的執行畫面:

圖(五):

 image

此範例程式筆者在後會提供下載,所以頁面上就不列出完整程式碼,只列出重點部份,否先我們在private區段宣告CAutoInterface為IAutoInterface介面:

private
    CAutoInterface: IAutoInterface;

下面則為TBitBtn的OnClick程式碼:

procedure TForm1.btnSendMsgClick(Sender: TObject);
begin
try
    try
        if RadioButton1.Checked then
       
//取得App1 CoClass物件,查詢並取得是否有我們所需要的IAutoInterface介面
        CAutoInterface := createoleObject('App1.App1') as IAutoInterface;
        if RadioButton2.Checked then
       
//取得App2 CoClass物件,查詢並取得是否有我們所需要的IAutoInterface介面
        CAutoInterface := createoleObject('App2.App2') as IAutoInterface;
       
//呼叫各自實作的SendMessage method.
        CAutoInterface.SendMessage(edMessage.Text, Form1.ClassName);
    except
        on E: Exception do
        ShowMessage('建立元件發生錯誤.系統訊息=' + E.Message);
    end;

    finally
    // CAutoInterface._Release;
end;
//
end;

如程式碼各位若仔細看會發現我們宣告的是相同的CAutoInterface介面去接App1和App2的IDispacth,透介面的轉型可以在RunTime時才決定是要使用App1或App2所支援的IAutoInetrface介面,並查詢其是否有支援我們所需要的IAutoInterface介面,Delphi的 as運算元會在背後執行COM基礎介面的QueryInterface方法,並傳回正確的介面參考,有些早期的Delphi程式碼可能會分成兩行寫成如下,其意義是相同的:

var
CDispatch: IDispatch;
CAutoInterface: IAutoInterface;
begin
    CDispatch := createoleObject('App1.App1');
//建立Ole Object
    CAutoInterface := CDispatch as IAutoInterface;
//取得所需介面
end;

5.測試範例程式

說了這麼多終於可以開始來實際測試這個範例應用程式了,這裡共會有CPAutoInterface、App1、App2個範例應用程式,我們先執行App1和App2應用程式,接著在執行提供父介面的CPAutoInterface如下圖,我們順利的將訊息傳送給CPAutoInterface的子介面App1和App2應用程式:

image

透過上面的希望對大家在COM對多型的支援在物件導向觀念的應用會有更深的了解,對於有在使用Delphi開發或使用COM有一些幫助,還有謝謝能夠將文章看完的朋友們,若有任何問題歡迎高手們指教。

6.範例應用程式下載

(1).CPAutoInterface.

(2).App1

(3).App2

註:執行註冊時請先註冊CPAutoInterface,在註冊App1和App2。

留言

這個網誌中的熱門文章

什麼是 gRPC ?

什麼是 gRPC(二)- 來撰寫第一個 Hello World 吧!

常見的程式碼壞味道(Code Smell or Bad Smell)