作者:巴哈姆特
http://www.cnpack.org
(轉載請注明出去并保持完整)
前面說過的封裝其實是邏輯意義上的封裝。邏輯封裝是對某一特定邏輯功能模塊的封裝,這個特定邏輯功能塊可以是一個類,當然也可以是一個包,他們都有自己的邏輯邊界。另一種封裝方式,我們通常叫它為物理封裝:物理封裝其實是具體實現代碼的物理集合,他可以以bpl,dll,com+等形式體現。
??? 邏輯封裝里,對象的傳遞、數據共享與調用相對要簡單的多,只要我們引用類所定義的單元(unit)就可以直接訪問類中public和published所公布出來的屬性或方法,在編譯的時候,編譯器會把工程內所有引用的單元全部打包到exe中。邏輯封裝最終是以一個獨立的物理文件存在的。雖然簡單,但是無法實現物理上的切割,一旦其中某個單元或代碼段發生改動,那么其他的單元或代碼段也需要重新編譯和連接。
??? 而在物理封裝中,對象的傳遞、數據共享與調用要復雜的多了,由于在編譯的時候,exe和dll或bpl是兩個或多個文件,所以你無法像在邏輯封裝中那樣簡單的uses那個unit。而物理封裝的好處是可以減少維護量,因為每個dll都是動態調用的,所以,我們只需要更新我們改動過的相應的dll,而其他的部分則可以不用改動。
? 用DLL封裝對象:
??? 用DLL封裝函數,我想幾乎是所有程序員熟悉到不能再熟悉的技術,而且我們可以找到很多相關的書籍和資料。這里我們只討論怎么用DLL來封裝對象。
??? 用DLL封裝對象有以下的好處:
????? 一、可以節約內存。我們可以在使用到DLL資源的時候動態裝載,不用時釋放。
????? 二、提高代碼重用。DLL在封裝好以后,我們可以使用任何一個支持DLL的開發工具來調用它。
????? 三、可以使軟件拆分成若干個小塊,這樣可以有效的降低維護量。
??? 注意:如果你只為了減少軟件體積而使用動態庫,那么我建議你還是放棄使用動態庫吧。
??? 當然,想使用DLL封裝對象也有一定的困難:
????? 一、調用DLL的EXE只能使用DLL中對象的動態綁定的方法。
????? 二、DLL中的對象只能在DLL中創建。
????? 三、在DLL和調用方,都需要對封裝的對象和被調用的方法進行聲明。
??? 我們來看下面的例子:
??? 首先我們聲明一個類:
typeTNewClass = class(TObject)publicprocedure SayHello; virtual;// 注意,這里不能使用靜態方法,必須使用動態綁定(或者說晚綁定)技術。// 至于為什么——虛方法表有關,大家可以找其他資料詳細研究^_^end;procedure TNewClass.SayHello; // 實現部分
beginShowMessage('Hello');
end;
?
新建一個Library項目
library dll;function GetObj: TNewClass; stdcall;
begin // 創建對象Result:= TNewClass.Create;
end;exports GetObj; // 定義輸出函數
end.
?
下面,我們創建一個EXE工程,并且添加類的聲明:
typeTNewClass = class(TObject)publicprocedure SayHello; virtual; abstract;{ 注意這里的聲明方式和DLL中的不同,這里必須聲明為virtual方法,還有由于此方法通過晚綁定用的是DLL中的實現,因此EXE中可不寫其實現而聲明成abstract方法。 }end;function GetObj: TNewClass; stdcall; external 'dll.dll';
?
之后,我們可以添加一段測試代碼來測試我們是否實現了DLL對象的共用:
varNewClass: TNewClass;
beginNewClass:= GetObj;if not Assigned(NewClass) thenExit;tryNewClass.SayHello;finallyFreeAndNil(NewClass);end;
end;
?
我們可以看到,這的確達到了EXE與DLL之間傳遞對象的目的。
??? 但是,有點麻煩:首先,在DLL工程與EXE工程都需要有被封裝對象的定義。其次,virtual和abstract必須正確使用。還有,如果一旦對象發生變化,那么兩邊的定義都需要修改,這樣難免會出點小錯。
??? 其實,我們可以使用接口來進行對象的傳遞,上面的例子我們可以稍微修改一下:
??? 首先,我們定義一個接口:
typeINewInterface = interface(IInterface)procedure SayHello(); // 定義我們要的方法end;
?
另外,修改TNewClass類的聲名:
typeTNewClass = class(TInterfacedObject, INewInterface)publicprocedure SayHello();end;
?
實現部分無須改動。
??? 接著,我們修改先前的那個Library項目:
library dll;function GetObj: INewInterface; stdcall;
begin // 創建對象Result:= TNewClass.Create;
end;exports GetObj; // 定義輸出函數
end.
在EXE工程中,我們直接引用接口定義的單元,并且修改輸出函數的聲明:
function GetObj: INewInterface; stdcall; external 'dll.dll';
?
之后,測試代碼會成這樣:
varNewInterface: INewInterface;
beginNewInterface:= GetObj;NewInterface.SayHello;NewInterface:= nil;
end;
?
這樣做的好處是,我們可以避免在多處重復說明一個要傳遞的對象的聲明,只要我們需要的方法的聲明方式不動,我們只需要改動TNewClass的實現代碼,而無需改動EXE程序中的任何代碼部分。
??? PS:Delphi的OpenToolsAPI接口就是這個通過接口共享對象原理的很典型的應用。(這是劉嘯說的。老實說,這個用法是我在寫這個筆記的時候臨時想到的,因為從來沒有使用過未經COM封裝的interface。哪里知道竟然和OpenToolAPI一樣的原理,自己YY下^_^)
??? 當然我們還可以使用COM來封裝對象:
??? 首先,我們建立一個名為NewCom的COM模型,建立COM模型前一篇已經說過,這里不再重復。
??? 那么,我們的調用代碼就會變成這樣:
varNewCom: ITNewCom;
beginNewCom:= CoTNewCom.Create;// 當然,和我前一篇一樣使用CreateComObject函數也是一樣的NewCom.SayHello;NewCom:= nil;
end;
?
我們可以看到,實現代碼幾乎沒什么改動。那么,假如我們什么時候要把SayHello的實現代碼:
ShowMessage('Hello');
?
改成:
MessageBox(0, 'Hello', 'SayHello', MB_OK);
?
??? 那么,我們只需要更新這個COM文件,調用它的EXE程序無須改動,這就是接口的優點。