CSDN博客

img rh

DLL 應用 - 設計可抽換的模組

发表于2002/6/5 10:28:00  1350人阅读

分类: 2002年前的转贴

DLL 應用 - 設計可抽換的模組

作者:蔡煥麟
日期:Jan-2-2001 

摘要:介紹以 DLL 來切割應用程式的實作方式,其中包含介面程式設計的技巧以及運用 Design Patterns 來解決設計上的問題。

前言

DLL(Dynamic Link Library,動態聯結函式庫)就目前來講已經不是什麼了不得的技術,坊間書籍隨手撿一本視窗程式設計或 Delphi 的書籍都可以找到 DLL 的相關說明,少這一篇也不算少,之所以寫這篇文章,一方面是給自己的學習心得作個記錄,一方面也提供給有需要的人參考;而本文的主題--設計動態載入的模組--說穿了也只是提供一個把 Form 包在 DLL 裡面的實作方法,儘管如此,我還是希望你能在其中發現一些比較不一樣的東西。

由於現有關於 DLL 的文件資料已經很多,在此不多做重複,因此在閱讀本文時會需要一些 DLL 的基礎知識或者 DLL 的撰寫經驗,這樣閱讀起來會比較輕鬆。以下就重點式地列出一些基礎觀念:

  • 靜態連結與動態連結的差異。
  • 了解如何宣告 DLL 輸出函式(exported functions)以及如何在外部呼叫它們。
  • 各種呼叫慣例(calling conventions)的差異。
  • 何謂 DLL hell(1)以及它對應用程式的維護有何影響。

DLL 在使用上又有靜態與動態載入的區別,所謂「靜態載入的 DLL」意指在編譯時期已經確定要連結的 DLL 是哪一個,而且會在行程初始化的階段就被載入,Delphi 的 VCL packages 即屬此類。動態載入的 DLL 則是執行時期需要時才載入,在程式撰寫上比靜態載入的方式麻煩些,但較有彈性,且應用程式啟動的速度也較快。

本文所要討論的就是以動態載入的 DLL 來實作可抽換的應用程式模組。

以 DLL 切割應用程式

一般來說,使用 DLL 有下列優點:

  • 節省記憶體。多個應用程式使用同一個 DLL 時,該 DLL 只會被載入一次,甚至可以用到時才載入 DLL,且用完立即釋放。
  • 程式碼重複使用,可讓不同的程式語言使用。
  • 應用程式模組化,可降低應用程式的複雜度,程式更新維護時較方便。
  • 可支援設計多國語言的應用程式。你可以把每一種語言的字串資源分別存放在一個 DLL 裡面,程式執行時便可以動態切換程式所使用的語言。

但也會一些困難必須克服,當我們要將應用程式切割成數個 DLL 模組的時候,通常會碰到以下幾個問題:

  1. DLL 如何輸出(export) VCL 物件?
  2. 如何將一個 Form 包在 DLL 裡面以供外部使用?
  3. DLL 之間如何共享變數?

基本上,如果你撰寫成 package 的形式就沒有上述問題了,但你可能會遇到其他麻煩,例如:名稱衝突的問題,這包括了型態、單元名稱、函式名稱的衝突,在此之前我也曾試著以 package 的方式來撰寫可抽換的模組,但名稱衝突的問題令我覺得蠻困擾。我也曾在另一份文件中提及此事,以下這段文字是從該文轉貼上來的(2):

「在撰寫幾個 package 的測試程式之後,我還是沒有將 package 應用在實際的專案開發中,而仍然使用 DLL,其最主要的原因,正是 package 優於 DLL 之處--可以共享變數。這項功能的立意很好,但也帶來了另一些限制,主要是名稱衝突的問題,使得共用的 unit 一定要放在 package 裡面,否則當兩個 package 包含了相同的 unit,其中一個就無法載入,我們覺得這會造成麻煩。另外,由於其他的小組成員對於 package 的使用不熟,容易出 trouble(例如:project 要加入 .dcp 之類的),這也是考量之一。」

在 DLL 之間共享變數的問題可以透過記憶體映射檔(memory-mapped file)來解決,你可以在文後所附的參考資料中找到相關資訊,這裡就不贅述。而在 DLL 中輸出 VCL 物件(例如:string)時得注意以下幾點:

  • 在 DLL 和其用戶端程式的 Uses 子句裡頭的第一個單元必須是 ShareMem。
  • BORLNDMM.DLL 必須跟著你的應用程式一起發佈。
  • 如果你修改了輸出物件的類別定義而使得原有物件的記憶體佈局改變,比如說加入一個 Integer 型態的私有成員,用戶端程式就必須重新編譯,如果使用舊的用戶端程式來呼叫新的 DLL 函式,應用程式就會發生錯誤甚至導致當機。

與其隨時注意這些規則,也許選擇可以完全避開這些問題的方法會比較好,我的意思是使用 Windows 的標準型別來傳遞資料,例如要傳遞字串,就用 PChar 來代替 string。對其他較為複雜的結構,可以使用介面來解決,這意味著兩件事情:

  1. 輸出的型態是個抽象類別(abstract class)或介面(interface)。
  2. 物件應由 DLL 來建立(用戶端程式不知道物件的記憶體佈局)。

符合了以上的規則,對於如何將 Form 物件包在 DLL 裡面的問題也就迎刃而解,稍後就會講到這部分如何實作。

介面(interface)在物件導向的領域裡是一個很重要的觀念,它描述了服務提供者和使用者之間的權責,或者說定義了兩個物件之間溝通的方式,通常這個溝通方式一經制定就不會修改(理想狀況下),因此介面亦可視為物件之間的合約。以 OOP 的角度來看,介面就是一組公開的方法(public methods),跟類別不同之處是它沒有 private 及 protected 等存取等級的區別,不可以包含資料成員,也沒有實作的程式碼,它就只是很單純的....呃...介面。在一個複雜的系統裡面,這種單純顯得特別珍貴,常常得經過一番深思熟慮之後才能萃取出較為抽象的成分,這不但有助於你在設計時以比較抽象的層次去思考,同時設計出來的介面也比較能夠再拿來重複使用。以用戶端的角度來看,介面把系統背後的複雜度隱藏了起來,用戶端就只需專注在它需要的部分,使用上會比較容易。

設計可抽換的模組

所謂可抽換的模組,就是指在程式執行時動態地載入與釋放的模組,對於規模較龐大,功能較複雜的應用程式來說,將應用程式切割成數個獨立運作的模組有以下優點:

  • 應用程式部署的組態更加彈性(例如:有些模組僅包裝於某種版本中)。 
  • 減少應用程式每次更新版本的檔案大小。 
  • 有利於明確劃分小組成員的權責。 
  • 有效地降低單一程式的複雜度,程式較易於維護。 

以下會一步步實作出一個具體而微的範例,你可以把這個範例視為一個基礎的框架(framework),稍加修改就可以運用於實際的專案開發上面。

描述需求

讓我們來簡單地分析一下應用程式的需求,假設原本的開發方式是將所有的程式單元編譯連結成一個可執行檔,現在要將應用程式的各個功能切割為獨立的模組,例如:

                     +--- 客戶資料維護作業(Customer.DLL)
主程式(Main.exe)---+--- 產品資料維護作業(Employee.DLL)
                     +--- 訂單資料維護作業(Orders.DLL)

其中每個 DLL 都是在使用者執行該項功能的時候才動態載入,而且每個 DLL 裡面至少包含一個 Form,為了有別於一般的 DLL,以下就以 plugin 稱之。我們預期各 plugin DLL 所包含的 Form 會有一些共同的屬性和行為,因此把這些共同點放到一個基礎視窗類別裡面,讓其他 Form 繼承自這個基礎類別。它們的關係看起來像這樣:

每一個維護作業都需要開啟一個視窗,因此主程式的責任之一便是建立並顯示 DLL 裡面的視窗。我們希望每一個維護作業的視窗關閉後才能執行另一個維護作業,所以使用 ShowModal 的方式顯示視窗。做一些簡單的分析之後,可以得到主程式在執行每項作業時所需的共同步驟:

  1. 載入指定的 plugin DLL。 
  2. 建立並顯示 plugin DLL 裡面的 Form 物件。 
  3. 釋放 Form 物件。 
  4. 釋放 plugin DLL。 

其中載入與是釋放 plugin DLL 的工作由主程式負責,而前面有提過 DLL 中的物件必須由 DLL 自己來建立,因此建立、顯示以及釋放 Form 物件的工作都由 plugin DLL 來負責提供函式,主程式只要在適當時機去呼叫它們就行了。

主程式

在主程式中加入一個執行 plugin 的方法,此方法需要一個參數指定 DLL 的檔名以便將其載入執行,像這樣:

procedure TMainForm.RunPlugin(const FileName: string);
var
  ADllHandle: THandle;
  APlugin: IPlugin;
  AFormHandle: THandle;
begin
  ADllHandle := SafeLoadLibrary(FileName);
  if ADllHandle <> 0 then
  begin
    APlugin := DllCreatePlugin(ADllHandle, Application.Handle);
    try
      AFormHandle := APlugin.CreateForm(Handle);
      APlugin.ShowModalForm;
      APlugin := nil;
      FreeLibrary(ADllHandle);
    except
      FreeLibrary(ADllHandle);
      raise;
    end;
  end
  else
    ShowMessage('無法載入函式庫: ' + FileName);
end;

從以上程式碼可以看出主程式載入 DLL 之後會呼叫 DllCreatePlugin 來建立 plugin 物件並且取得其介面參考,接著主程式就利用該介面參考來存取 plugin 物件提供的服務,包括建立視窗,顯示視窗等等。很明顯地,IPlugin 介面是主程式和 plugin DLL 之間溝通的橋樑,而且 IPlugin 介面至少要提供下列方法:

CreateForm - 建立 Form 物件
ShowModalForm - 顯示視窗
DestroyForm - 摧毀 Form 物件

眼尖的讀者可能會發現,上面的程式中並沒有呼叫 DestroyForm,而且也沒有呼叫類似 DllDestroyPlugin 的函式來摧毀 plugin 物件,這些物件什麼時候會被釋放掉?

它們是自動被釋放掉的。由於 Form 物件的建立是透過 plugin 物件來完成,所以我打算把摧毀 Form 物件的責任交給 plugin 物件,也就是當 plugin 物件摧毀時會自動將 Form 物件一併釋放掉;而為了簡化摧毀 plugin 物件的動作,我讓 plugin 物件具有自動參考計數的能力,這麼一來只要該物件沒有人使用它(物件的參考計數為 0)就會自動釋放掉了,做法很簡單,只要讓實作 IPlugin 的類別繼承自 TInterfaceObject 就行了,其他細節都由 VCL 幫我們完成了。

LoadLibrary 與 FreeLibrary 也有自己的參考計數,並且用它來決定是否載入及釋放 DLL。也就是說重複呼叫 LoadLibrary('A.DLL') 並不會將 A.DLL 載入兩次,第二次的呼叫只會遞增參考計數而已;同樣的,FreeLibrary 會遞減 DLL 的參考計數,直到計數為 0 才會真正將 DLL 釋放掉。

接著看 DllCreatePlugin 函式:

type
  TCreatePluginFunc = function (hApp: THandle): IPlugin; stdcall;

const
  SDllCreatePluginFuncName = 'CreatePlugin';

implementation

resourcestring
  sErrorLoadingDLL = '無法載入模組!';
  sErrorDllProc = '無法呼叫 DLL 函式: %s';

function DllCreatePlugin(hLib, hApp: THandle): IPlugin;
var
  pProc: TFarProc;
  CreatePluginFunc: TCreatePluginFunc;
begin
  pProc := GetProcAddress(hLib, PChar(SDllCreatePluginFuncName));
  if pProc = nil then
    raise Exception.CreateFmt(sErrorDllProc, [SDllCreatePluginFuncName]);
  CreatePluginFunc := TCreatePluginFunc(pProc);
  Result := CreatePluginFunc(hApp);
end;

DllCreatePlugin 會嘗試從指定的 DLL 模組中呼叫函式 'CreatePlugin' 來建立 plugin 物件,並且傳回 plugin 物件的介面參考,參數 hLib 是 DLL 代碼,而 hApp 則直接傳遞給 DLL 的CreatePlugin 函式,這個參數的作用稍後會解釋。

至此主程式所需的程式碼大致上已經完成了,接下來看看 DLL 的 CreatePlugin 函式。

DLL 的輸出函式

我們的 plugin DLL 只有輸出一個函式供外界呼叫,就是前面提到的 CreatePlugin,其函式原型為:

function CreatePlugin(hApp: THandle): IPlugin; export; stdcall;

CreatePlugin 函式會建立 TPlugin 物件並且傳回 IPlugin 介面的參考。由於 plugin 物件僅需被建立一次,我們可以用一個全域變數實作出簡單的 Singleton3):

var
  g_PluginIntf: IPlugin = nil;

implementation

function CreatePlugin(hApp: THandle): IPlugin;
begin
  Application.Handle := hApp; // EXE DLL 使用同一個 application handle.
  if g_PluginIntf = nil then
    g_PluginIntf := TPlugin.Create; // TPlugin 的物件參考計數 = 1
  Result := g_PluginIntf;           // TPlugin 的物件參考計數 = 2
end;

CreatePlugin 需要傳入一個參數 hApp,代表呼叫者程序的 Application 物件的 Handle,通常是傳入 Application.Handle,好讓主程式和 DLL 的 Application 物件能夠「同步」。之所以要這麼做是因為當你的 DLL 專案未使用 "Build with runtime package" 選項時,執行檔和載入的 DLL 會各自有一個 Application 物件,但是只有執行檔的 Application 物件有連結一個視窗,DLL 則沒有,因此 DLL 的 Application.Handle 屬性總是為 0。若少了這個同步的動作,那麼當 DLL 的 Form 開啟時,你會在桌面的工作列上看到多了一個視窗按鈕,看起來就像執行了另一個應用程式一樣,我們不希望看到這種情形。

當然啦,如果你的主程式和 DLL 都使用 "Build with runtime packages" 來建立(你應該這麼做),就不需要這個同步動作了(想想看為什麼?)。

程式碼裡面有兩行關於物件參考計數的註解,是想要表達介面程式設計的一個基本觀念:當一個介面參考在函式之間以 pass by value 的方式傳遞時會遞增物件的參考計數(pass by reference 則不會)。此觀念有助於你正確掌握物件的壽命。

最後別忘了還要把這輸出函式加到專案原始碼的 exports 子句裡頭:

exports
  CreatePlugin;

IPlugin 介面與 TPlugin 類別

IPlugin 介面定義如下:

IPlugin = interface
['{D3F4445A-C704-42BC-8283-822541668919}']    // Ctrl+Shift+G 產生 GUID
  function CreateForm(hMainForm: THandle): THandle;
  procedure DestroyForm;
  function ShowModalForm: Integer;
end;

其實以上函式也可以寫成一般的 DLL 函式,當作是 DLL 的介面,之所以另外定義這個介面,一方面是希望簡化 DLL 本身的介面,另一方面也可以集中管理程式碼,以後如果需要增加介面方法的話,只要加在 IPlugin 介面裡面就好了,不用把現有的 DLL 原始碼一個個找出來修改,這也有助於簡化 DLL 的撰寫以及日後的維護工作。

介面不包含實作,實作必須由類別來提供。

接著定義一個 TPlugin 類別來實作 IPlugin 介面:

TPlugin = class(TInterfacedObject, IPlugin)
private
  FForm: TForm; 
public
  destructor Destroy; override;

  function CreateForm(hMainForm: THandle): THandle;
  procedure DestroyForm;
  function ShowModalForm: Integer;
end;

在 IPlugin 中加上 GUID 以及讓 TPlugin 繼承自 TInterfacedObject 的目的,是為了讓物件擁有 Interfaced RTTI 以及自動參考計數的能力,這樣我們的 TPlugin 物件就會在沒有任何人使用它時自動釋放掉。私有成員 FForm 記錄了此 plugin 物件所建立的視窗的參考,以便控制其壽命,其型態也可以視需要改成 TBaseForm,那麼你的 TBaseForm 的設計得盡量不要經常修改,或者說設計得抽象一些,讓這些核心的類別在比較抽象的層次上面運作。

各個方法的名稱皆可望文生義,程式碼也很簡單,相信你可以猜個八九不離十,這裡就不一一列出,比較值得一提的是 CreateForm 函式與解構元 Destroy,分述如下:

TPlugin.CreateForm - 使用類別參考來建立物件 

在 CreateForm 函式裡面,建立 Form 物件的那行程式是這麼寫的:

  FForm := g_ConcreteClass.Create(Application);

其中 g_ConcreteClass 是一個全域變數,其定義為:

var
  g_ConcreteClass: TBaseFormClass := nil;

而 TBaseFormClass 是一個類別參考型態(class-reference type),它跟 TBaseForm 定義在同一個單元裡面:

type
  TBaseFormClass = class of TBaseForm;

  TBaseForm = class(TForm)
  .....
  end;

也就是說我們用一個類別參考型態的變數 g_ConcreteClass 來記錄欲實體化的類別型態,因此在建立 Form 物件之前還必須先設定 g_ConcreteClass 才行,如此 TPlugin 才能以正確的 Form 類別來進行實體化的動作。

您或許會想為什麼要這麼麻煩,直接寫成像 TCustomerForm.Create 這樣不就好了嗎?

簡單地說,是基於維護的考量。由於在整個 TPlugin 的實作裡面,日後唯一可以能會經常變動的就是要被實體化的 Form 類別,使用類別參考使我們免於在 TPlugin 的實作程式碼裡面把類別型態寫死,以後如果要實體化其他的 Form 類別,只要修改 g_ConcreteClass 這個變數就行了,不用再費一番搜尋及替換文字的功夫,還得擔心有沒有哪裡沒有改到;換句話說,我們等於使用類別參考來讓編譯器幫我們完成這個替換文字的動作,而且保證不會遺漏任何地方。

我交替使用了「建立物件」與「實體化」兩種詞彙,其實它們指的是同一件事情:建立某個類別的實體(instance)。

此技巧對於團隊開發也有好處,你只要公佈 TPlugin 和 TBaseForm 兩個單元,然後告訴組員照下面兩個步驟做就行了:

  1. 從 TBaseForm 衍生一個新類別(可以利用 Delphi 的物件寶庫來簡化這項工作)。
  2. 在這個新類別的單元的 Uses 子句裡加入 TPlugin 類別所屬的單元,並且在初始化階段把類別名稱指定給 g_ConcreteClass 變數。

在這個範例裡面,我們只有一個 TBaseForm 的後代,叫做 TForm1,因此在 TForm1 的單元裡面會有這一段:

uses
  DllExport;  // TPlugin 類別實作放在這個單元裡面

.....

initialization
  g_ConcreteClass := TForm1;

 

TPlugin.Destroy

解構函式會呼叫 DestroyForm 使 Form 物件一併釋放掉,並且還原 DLL 的 application handle:

destructor TPlugin.Destroy;
begin
  DestroyForm;
  Application.Handle := g_DllAppHandle;
  inherited Destroy;
end;

其中 g_DllAppHandle 是一個全域變數,其宣告如下:

var
  g_DllAppHandle: THandle;

而我們必須在 DLL 初始化的時候將 DLL 本身的 application handle 保存起來:

initialization
  g_DllAppHandle := Application.Handle;

其實如果 DLL 專案有用 "Build with runtime package" 選項的話,這個保存及還原 application handle 的動作就可以免了。相反地,若不加上保存及還原的動作,而且 DLL 專案不使用 "Build with runtime package" 選項的話,當 DLL 被釋放時就會發生主視窗也被一併關閉的怪異情形。

擅用原始的力量

到此重要的部分應該都已經提到了,您可能會發現我並沒有對 TBaseForm 多做說明,原因是在這個範例程式中 TBaseForm 並沒有什麼特別之處,只是為日後擴充時預留的一個基礎類別,你也許會想要將各個模組共用的功能和視覺化介面集中在此類別以簡化各模組的撰寫工作,以及讓應用程式有一致的操作方式和行為,這部分每個人的需求不同,就請您自行發揮了。

如果你覺得以上的程式碼過於片段零散,無法獲得整體的概念,建議您直接看範例的原始碼,把範例程式執行一遍以觀察程式運作的過程,不了解的地方再回來文件裡尋找解釋,這樣也許會比較容易些。為了方便閱讀,我也把範例程式中比較重要的兩個單元分別列在表一和表二裡面了。

列表一. DllUtils.pas
unit DllUtils;

interface

uses
  Windows, Messages, SysUtils, Classes, Forms, Controls;

type
  IPlugin = interface
  ['{D3F4445A-C704-42BC-8283-822541668919}']  
    function CreateForm(hMainForm: THandle): THandle;
    procedure DestroyForm;
    function ShowModalForm: Integer;
  end;

  TCreatePluginFunc = function (hApp: THandle): IPlugin; stdcall;

function DllCreatePlugin(hLib, hApp: THandle): IPlugin;

implementation

resourcestring
  sErrorLoadingDLL = '無法載入模組!';
  sErrorDllProc = '無法呼叫 DLL 函式: %s';

const
  SDllCreatePluginFuncName = 'CreatePlugin';

function DllCreatePlugin(hLib, hApp: THandle): IPlugin;
var
  pProc: TFarProc;
  CreatePluginFunc: TCreatePluginFunc;
begin
  Result := nil;
  if hLib = 0 then
    Exit;
  pProc := GetProcAddress(hLib, PChar(SDllCreatePluginFuncName));
  if pProc = nil then
    raise Exception.CreateFmt(sErrorDllProc, [SDllCreatePluginFuncName]);
  CreatePluginFunc := TCreatePluginFunc(pProc);
  Result := CreatePluginFunc(hApp);
end;

end.

 

列表二. DllExport.pas
unit DllExport;

interface

uses Windows, Classes, Forms, DllUtils, BaseFrm;

type
  // Inherited from TInterfacedObject to be reference-counted.
  TPlugin = class(TInterfacedObject, IPlugin)
  private
    FForm: TBaseForm;
  public
    destructor Destroy; override;

    function CreateForm(hMainForm: THandle): THandle;
    procedure DestroyForm;
    function ShowModalForm: Integer;
  end;

function CreatePlugin(hApp: THandle): IPlugin; export; stdcall;

exports
  CreatePlugin;

var
  g_ConcreteClass: TBaseFormClass := nil;
  g_PluginIntf: IPlugin = nil;
  g_DllAppHandle: THandle;

implementation

uses Dialogs, SysUtils;

function CreatePlugin(hApp: THandle): IPlugin;
begin
  if hApp <> 0 then
    Application.Handle := hApp;     // Sync Application handle.

  if g_PluginIntf = nil then
    g_PluginIntf := TPlugin.Create; 
  Result := g_PluginIntf;           
end;

{ TPlugin }

destructor TPlugin.Destroy;
begin
  DestroyForm;
  Application.Handle := g_DllAppHandle;
  inherited Destroy;
end;

function TPlugin.CreateForm(hMainForm: THandle): THandle;
begin
  if FForm = nil then
  begin
    Assert(g_ConcreteClass <> nil, '未設定欲實體化的 Form 類別名稱!');
    FForm := g_ConcreteClass.Create(Application);
    FForm.MainFormHandle := hMainForm;
  end;
  Result := FForm.Handle;
end;

procedure TPlugin.DestroyForm;
begin
  if FForm <> nil then
  begin
    FForm.Release;
    FForm := nil;
  end;
  Application.ProcessMessages;
end;

function TPlugin.ShowModalForm: Integer;
begin
  if FForm = nil then
    raise Exception.Create('DllExoprt: 視窗尚未建立!');
  Result := FForm.ShowModal;
end;

initialization
  g_DllAppHandle := Application.Handle;

end.

 

範例程式

範例程式可以按此處下載:PluginDLL.zip

下載壓縮檔並解開後,請先閱讀其中的 readme.txt。

可改進之處

你可以試著修改範例程式並強化它,使它可以當作實際開發專案的基礎框架,以下列出幾項可能的改進之處:

  • 賦予 TBaseForm 基本的資料處理能力,像是新增、修改、刪除...等。
  • 修改使之適用於 modeless form 及 MDI 應用程式。這意味著釋放 DLL 的時機也會改變,你可能會需要一個串列結構將載入的 DLL 記錄起來,通常一個 TStringList 就可以做到。
  • 讓一個 plugin 物件可以建立並維護多個不同類型的 Form 物件。

你可能會希望一個 DLL 裡面可以提供多種 form 物件供主程式使用,這些 form 物件之間可能有某種程度的相似或相依關係。根據此需求我們可以整理出 plugin 物件具備以下兩個特性:

  1. plugin 物件可以建立多種不同類型的 form 物件,而它們都是繼承自基礎的表單類別 TBaseForm。
  2. 一個 DLL 裡面只需要一個 plugin 物件。

根據  [GHJV95] 書中的定義,Abstract Factory 的用意是:

「提供一個介面來建立同一族系或相依的物件,而毋須指明它們的具象類別(concrete class)」

而 Factory 通常也被實作成 Singleton,這些特性清楚地告訴我們 plugin 物件非常適合實作成一個 Factory。你可能需要在 TPlugin 類別裡面提供一個 RegisterClass 方法,這個方法取代了原先的類別參考型態,原本在 TBaseForm 子類別的單元裡設定 g_ConcreteClass 的敘述將會改成:

  PluginFactory.RegisterClass(TForm1);

註冊過的類別資訊將會被記錄在一個串列裡面。主程式則可以在建立 form 物件時透過字串來指定要建立的 form 類別名稱,像這樣:

  APlugin.CreateForm('TCustomerForm');

plugin 物件的 CreateForm 方法就會到串列中搜尋註冊過的類別,取得對應的類別參考並建立其實體(是不是有點像 COM 所做的事情?)。

嗯,我想這樣的提示應該夠了,最重要的還是要自己實際去撰寫及除錯程式碼以獲得更深刻的體會,真能如此,這個 Design Pattern 就會完全融入你的知識體系裡面,以後不加思索便可以運用自如了。

結語

在這份文件裡面主要是介紹以 Delphi 來設計 plugin 模組的實作過程,其中運用了介面程式設計的技巧(包括介面的參考計數以及物件生命週期的控制)以及 Design Patterns 來解決設計時遭遇的問題,這也是學習的重點之一。

在一個多人開發的專案裡,如果您的責任是設計主程式框架,當您要以 DLL 來切割應用程式時會怎麼做呢?這篇文章裡面展示了一種可能的設計方式,如果您有不同的想法或者對本文有任何建議,都很歡迎您來信指教。

Delphi 的 DLL 記憶體漏洞

最後,雖然不是本文的主題,但也頗值得注意的,就是動態載入的 DLL 在釋放時會有 4K 的記憶體漏洞,而且 Delphi 5 和 6 都有這個問題,你可以閱讀下面兩份文件,其中有詳細的說明並提供解決之道:

 

註1.

 

由於 DLL 版本的更新可能使得原本叫用它的程式無法正常運作,因此以不同的檔名區分版本(例如:MFCxx.DLL),使得硬碟裡面必須保存同一種 DLL 的多個版本,即使使用者將應用程式移除了,卻不敢放心的移除相關的 DLL 檔案,以免其他應用程式因為缺少了這個檔案而無法運作,這種情況所形成的問題稱為 DLL hell。COM 的出現有解決此問題的企圖(透過執行時期詢問元件支援的介面),但似乎並不理想,直到 .NET 的問世而終於有了比較好的解決方案。
註2. 可以到 http://www.geocities.com/huanlin_tsai/ 的〔心得分享〕區找到相關文章。不可諱言,以上所說的難免摻雜了個人的因素,也許其他人在使用 package 時並未發生上述問題,而且使用 package 的方式也有許多優點,在此僅將個人實際應用時的狀況與感覺描述出來,若有謬誤之處尚請各方不吝指正。
註3. Singleton 樣式:提供單一窗口來建立類別的實體,以確保只有一個類別的實體存在。參考 [GHJV95] 的書。

參考資料

  • Delphi 學習筆記。作者:錢達智。碁峰資訊,1998。
  • [Cantu2001] Marco Cantu. Mastering Delphi 6. SYBEX, 2001.
  • [Harmon2000] Eric Harmon. Delphi COM Programming. MTP, 2000.
  • [GHJV95] E. Gamma, R. Helm, R. Johnson, J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995. 中文版:物件導向設計模式,葉秉哲。培生,2001。

0 0

相关博文

我的热门文章

img
取 消
img