Delphi Package 學習筆記(含討論信件精華)

作者: 蔡煥麟
日期: Mar-29-2000, Sep-3-2001

在實作 package 時,我遇到了一些問題,承蒙DelphiChat多位熱心朋友的幫忙解決小弟許多疑惑,因此當傅兄(James Fu)提議將討論信件整理出來並配合範例供大家參考時,我也很贊同,並且將平常的筆記及範例程式稍作整理一番,於是有了這份文件。

如果你正在考慮要不要將 package 實際運應用在專案裡面,你可以先看看 package 的優點缺點。想要進一步了解package 如何使用,建議您先閱讀參考資料裡面的一些文章,然後下載範例自己動手做看看。

參考資料

市面上已經有很多現成的資源,包括書籍,雜誌,網際網路上面都有介紹 package 的基礎知識,所以我並不想在這裡重複這些東西,我將這些資源列出來供需要的人參考:

以上所列的資源應該相當夠了,另外我也用 Delphi 5 寫了三個範例,分別是:

包含一個主程式及兩個 package ,壓縮檔內含子目錄,若以 Pkunzip 解壓縮時請加 -d 參數。其中ProjectGroup1.bpg 可以建立所有的模組。

PkgDemo1 的改進版本,壓縮檔內含子目錄,若以 Pkunzip 解壓縮時請加 -d 參數,解開後請先閱讀其中的 Readme.txt,裡頭有簡單的說明。
從這個範例應該可以衍生出更複雜的實際應用,如果有不了解之處, 我建議您先看看 James Heyworth 的那篇文章(見上方參考文獻)及其範例。

此範例將 DataModule 也做成一個獨立的 package,請參考 _readme.txt 的說明。

以下是網友提供的範例:

示範如何將 DataModule 放在 package 中讓其他程式共用。

以下是我的筆記,比較片段而缺乏完整性,僅供參考:)

使用 Package 的優點

使用 Package 的缺點

在將模組獨立出來的過程中會遇到共享全域變數及物件等問題,需要花比較多的心思在模組化的設計上面,關於這些問題,可以參考 DelphiChat 討論信件精華 或許能得到些線索。

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

動態載入 package 的函式(SysUtils.pas)

載入 package 至應用程式的執行空間。
函式原型:function LoadPackage(const Name: string): HMODULE;

參數 Name 為欲載入的 package 的完整路徑及檔案名稱。
傳回值為載入的 package 的 Handle,此 Handle 在釋放 package 時會用到,所以必須好好保存。

此函式會先檢查欲載入的 package 所包含的 units 是否已在目前載入的 package 中重複出現,若程式單元重複就不載入且顯示錯誤訊息,否則會將此 package 中所包含的各單元的 Initialization 區段的程式碼執行一遍。請注意,LoadPackage 會先檢查該模組是否已經載入了,如果是,就直接傳回該模組的Handle,並且將模組的載入計數加一(increase reference count),模組中各單元的Initialization 區段不會被執行。所以即使重複的呼叫此函式,我們並不會在程式的執行空間中擁有多個相同的模組。

函式原型:procedure UnloadPackage(Module: HMODULE);

此函式會先將指定的 package 模組所包含的各單元的 Finalization 區段的程式碼執行一遍,然後卸載該模組。
從 VCL 的原始碼 SysUtils.pas 中挖掘,可以發現它此函式依序呼叫了 FinalizePackage 及 FreeLibrary,其中 FinalizePackage 會去執行 Finalization 區段的程式碼,而 FreeLibrary 是 WinAPI,它會先遞減模組的參考計數,如果參考計數為零才會將模組釋放掉。

VCL 類別註冊函式 (Classes.pas)

procedure RegisterClass(AClass: TPersistentClass);
procedure RegisterClasses(AClasses: array of TPersistentClass);

透過串流系統(stream system)註冊類別,已註冊過的類別不會重複註冊,但如果是不同 package 有相同的類別名稱,則呼叫此函式會出現 EFilerError 的錯誤訊息。此函式通常放在程式的 initialization 區段。

透過 RegisterClass 註冊的類別可以用此函式取得類別的 MetaClass,取得 MetaClass 之後就可以以此來建立該類別的物件。參考下面的範例(以下展示的方式不只可以建立 Form 物件,稍作修改就可以建立其他物件):

function CreateFormByClassName(ClassName: string): integer;
var
  AClass: TPersistentClass;
  AForm: TCustomForm;
begin
  Result := mrNone;
  { Note that TApplication "owns" this form and thus it must
    be freed prior to unloading the package }
  AClass := GetClass(ClassName);
  if AClass <> nil then
  begin
    AForm := TComponentClass(AClass).Create(Application) as TCustomForm;
    Result := AForm.ShowModal;
  end;
end;

當卸載 package 時,類別可以明白地在 finalization 區段中註銷(Unregister),也可以隱含地使用 UnRegisterModuleClasses 函式。

1. 載入並初始化 package。
2. 使用 package。
3. 資源回收(釋放你從 package 中建立的物件)。
4. 註銷類別。
5. 卸載 package 。

注意事項及建議


Delphi Chat 討論信件精華

討論主題:應用程式模組切割問題
參與人員:Fang Lin, Hippy, James Fu, MR7, Maxwell Lin, Michael Tsai, Scott

這些討論信件是依照日期的先後順序排列,由於信件中引言所佔的比例蠻大的,所以我做了些刪改,目的是為了更易於閱讀,若有不當之處尚請見諒。

3-10-2000, Michael Tsai:

Dear all:
我最近嘗試將一個資料庫應用程式的專案切割成許多獨立的 package 模組,例如:客戶模組,廠商模組,進貨模組...等。
我使用了動態載入 package 的方式,並且寫了幾個練習用的測試程式,運作得還算好,只是有個問題:在實際的專案當中,為了求操作介面的統一,我大量的使用了視覺化繼承的 Form (aka VFI, Visual Form Inheritance),每一個資料維護作業的視窗都繼承自一個基礎的類別,例如:TBaseDataForm,但是 Delphi package 不允許兩個 package 裡面包含相同的 unit,這種限制讓我無法在各個 package 中使用 VFI,曾想過改用 DLL,可是 DLL 在全域變數上同樣得花番功夫,且各模組之間的參數傳遞也沒有 package 來得方便,不知各位有沒有什麼建議? 您建議將它們撰寫成 COM 物件嗎?
謝謝!

Michael Tsai

3-10-2000, Fang Lin:

Hi:
我也是採用這種做法. 也碰到相同的事情.

BasePackage 表示 TBASEDATAFORM所包成的package如果要視覺化繼承Form, 必須在Project Group中將BasePackage 加入,否則會出現 TBaseDataForm not found的錯誤訊息.
在新建立一個繼承自TDataBaseForm的Form時, 必須將Active Project(可能是dpk, or dpr) 設成 BasePackage, 然後在選擇 Main Menu /File/New 切換自BasePackage 的 TabSheet, 再選擇要繼承的 Form即可, 只是新增的Form會自動加到BasePackage中, 蠻討厭的.

不知道有沒有更好的方法?

Fang Lin

3-11-2000, Michael Tsai:

Hello, Fang:
我後來把 BasePackage 由 Runtime package 改為 Designtime and runtime package, 然後 install 這個 package, 之後便可以任何時候手動更改 Form 的父類別,做法如下:

1. New 一個標準的 Form,假設類別名稱為 TForm1。
2. 把 TForm1 = class(TForm) 改為 TForm1 = class(TMyForm)。
3. 按 Alt+F12 (View as text), 把第一行的 Object TForm1 ...改成 Inherited TForm1 ...。再按一次 Alt+F12。

雖然不完美,但總算有法子解決在 package 中視覺化繼承 Form 的問題了。如果要有個比較"乾淨"的解決方案,或許撰寫一個Form Expert 是最好的解決方式,關於這方面,Ray Lischner 有一本著作: Hidden Path Of Delphi 3,中譯本由李維先生翻譯,文魁出版,裡面有現成的範例與程式碼,應該稍微修改就能使用。不過我還是希望有其他比較簡單又漂亮的解決方案 ...

Michael Tsai

3-15-2000, Maxwell Lin:

TO Dear Michael:
您這樣的問題有點難說喔。因為能像您這樣架構程式的人不多,更枉論評析這種做法的優劣。
我在公司遭遇的困難是人力不足,除了一些舊有程式的維護工作之外;大部分工作都落在介面上的調整。真正與資料有關的BUSINESS RULE新程式其實比較少。因此比較傾向獨立business rule為共用元件,目前正朝著COM+方向。

至於畫面部分,就依據client來選。比如在lan上就用delphi做,在www上就用ASP + VBScript或ASP + JScript。

我經驗中:畫面共用性其實很低的,但這也只是我的想法啦,不一定是對的。

Maxwell Lin arus@tpts1.seed.net.tw

3-15-2000, Michael Tsai:

Hello, Maxwell:
關於畫面的共用性,我的想法是把常用的資料操作功能及畫面安排設計成基礎類別,這樣多人開發時,就省得寫畫面的 spec,只要每位程式員繼承自基礎視窗類別就好了。我的 Form 類別大約有以下幾種:

  • TBaseDataForm
    所有 Form 的父親, 有一個加強型的 DBNavigator。
  • TBrowseEditForm
    有一個 PageControl,內有兩個 TabSheet, 分別提供DBGrid 可供瀏覽及編輯欄位資料。
  • TMasterDetailFormA, TMasterDetailFormB...
    父子關聯視窗

如果要說明我的整個設計,恐怕得花上好些篇幅,容我概述一二:
在 TBaseDataForm 中所放置的增強型的 DBNavigator, 增加一些特殊的按鈕,如: 查詢,尋找,複製,列印..等,我讓 TBaseDataForm 內建了預設的資料操作的功能,並且將這些功能宣告為虛擬函式,讓繼承者可視需求更改原有的功能。
由於我最原始的目的是希望當資料庫的表格分析設計好之後,讓 coding 的速度加快,而在歷經時間考驗後,目前的設計也的確讓我覺得省事很多......直到我想要撰寫獨立的模組。
我想,在撰寫模組時,使用樣板(or 元件)要比繼承適合得多了,尤其是一層又一層的繼承也是蠻要命的,還是儘量避免得好。
關於畫面及操作的一致性,我想可能會因為軟體的性質不同而會有不同的考量吧,
我猜想您的程式是給公司內部人員使用的?

Regards,
Michael Tsai

3-16-2000, Hippy:

Hello,
提供您一個小建議:
因為本公司也是用package來做的。而且做的一切都還算挺順利(到現在為止)。只是,開發的順序和你有一些不同!
我們先將不同的模組各自開發(EXE形式)完成後,再把它 compile 成 Package。而且,我們在各個模組裡,也是有共用的form及使用繼承的技巧。
所以,簡單的說,也就是在開發的時候,先 uses 被繼承的 form,然後,新的form 再從那邊繼承下來!最後,再將共用的祖先 compile 成 .dcp ,然後再各自 compile 其他的模組,這樣,應該可以。建議您不妨試試...

Hippy

3-21-2000, MR7:

> 這幾天翻 Delphi 手冊時發現了一個編譯指示似乎跟Package有關,但是我沒試過
> ,不知道 {$WEAKPACKAGEUNIT ON} 是不是更直接的方法 ?

3-22-2000, James Fu:

Dear Friend :

>您這樣的問題有點難說喔。因為能像您這樣架構程式的人不多,
>更枉論評析這種做法的優劣。
>我經驗中:畫面共用性其實很低的,但這也只是我的想法啦,不
>一定是對的。

插個花一下,我想切割成好幾個模組這樣的架構應該不少,個人認為,有時為了維護上的方便,比方說您今天開發一個應用系統給您的客戶,用了幾天之後發覺有一個Form上面打了一個錯字或是一個處理寫錯了,也許您可以把所有的程式重新Complier成一個新的exe檔傳給客戶,即使您使用了 D4 or D5 的 RunTime Package 的方式,我想一個系統可能還是需要好幾MB。
如果此時您採用分割模組的方式的話,也許您只需要將有問題的程式部分重新編譯,只需要給對方這修改過的。
當然,還有許多情況可以使用到切割模組的好處,尤其是您需要開發到商業套裝軟體時,那就更顯得重要了。至於要利用什麼樣的技術來切割,在網路上有不少介紹利用 dll 的動態載入的方式,可以參考 QuickReport 的網佔有個不錯的 Sample,或是寬達兄的深度歷險中也有不少的範例,這些所使用的方法都大同小異,您可以選一個較為詳細的參考看看,這只是方法之一。至於其他的像是Package 的方式,或是載入 DFM 檔的方式也都可行。

至於畫面共用性我個人倒是覺得在系統中應該會很高,比方說您的系統中有 10 隻左右的輸入資料作業,您不大可能這幾支作業新增、刪除、修改之類的按鈕都設計的不同大小、位置吧。當然啦,這樣的情況下可能會讓畫面有些呆板,但對使用者來說或許會比較容易一點學習呢?

小弟才疏學淺,將一些個人的淺見願與大家分享,還望大家不吝指教。

James Fu

3-23-2000, Michael Tsai:

經過幾次的討論,我們知道撰寫獨立的模組的確有許多優點,茲整理如下:
- 應用程式可以被高度的模組化,而且可以逐漸交付完成的功能給客戶。
- 維護比較方便。
- 容易包裝成多種版本以因應不同的市場策略。
- 節省記憶體,並且提昇程式載入的速度。

...似乎沒有缺點:)

如果用 Delphi 及 BCB 開發的話,個人覺得還是用 Package 的方式切割會比較輕鬆,主要是因為在主程式與各模組之間很有可能需要傳遞參數,而這些參數可能是 VCL 物件,使用 DLL 的話會比較麻煩。另外,DLL 無法共用變數,而必須採用如 memory-mapped file 或其他方式來解決,也是要考慮的。

至於畫面共用,我贊同您的看法,事實上,我的程式大量使用視覺化的Form繼承關係,以確保操作介面的一致性。
順便一提,Kylix 會實做 Linux Shared Object, 也就是像 DLL 的東西,想請教熟 Linux 的朋友 .so 是否可以像 package 一樣共享全域變數?

Regards,
Michael Tsai

3-23-2000, James Fu:

Dear Friend :
> - 節省記憶體,並且提昇程式載入的速度。
對於這一點個人有些不同的看法,比方說在使用切割的方式,當程式在執行時如果是採用動態載入的方式的話,那速度會比 Complier 在一起慢不少,記憶體方面,Complier 在一起的時候也許剛執行的時候會佔的比較多,但當採用動態載入時如果將所有的都載入時,不見得佔的記憶體不見得會比較少。
但如果整個系統有上百隻程式,而每次執行時可能只會用到部分的程式時,在這樣的情況下也許會比較省記憶體。

>...似乎沒有缺點:)
應該還是有,比方說架構就會比較複雜,公用變數的處理,大量參數傳遞,物件的共用,MDI 較不容易做等。這些應該可以算是缺點吧 : P

>如果用 Delphi 及 BCB 開發的話,個人覺得還是用 Package
>的方式切割會比較輕鬆,主要是因為在主程式與各模組之
>間很有可能需要傳遞參數,而這些參數可能是 VCL 物件,
>使用 DLL 的話會比較麻煩。另外,DLL 無法共用變數,而
>必須採用如 memory-mapped file 或其他方式來解決,也是要
>考慮的。
我個人倒是比較偏向使用 DLL 的方式,使用 Package 的話,那只能做到切割程式時所有程式都是使用同一套開發軟體,但使用 DLL 就比較沒有這樣的限制,就像 WinAMP 可以提供一些介面外掛其他人所寫的東西一樣。
我不熟 Linux,但如果要做到處理共用變數,其實 DLL 不見得要用到像 memory-mapped file 那麼複雜的方式才有辦法做到,可以利用像 RTTI 或是一些繼承的技巧,也是可以很容易的做到的。
個人一點淺見,還望大家多多給予指教....

James Fu

3-25-2000, Michael Tsai:

Hello, James:

> > - 節省記憶體,並且提昇程式載入的速度。
> 對於這一點個人有些不同的看法,比方說在使用切割的方式,當程式
> 在執行時如果是採用動態載入的方式的話,那速度會比 Complier 在一起慢不少
> ,記憶體方面,Complier 在一起的時候也許剛執行的時候會佔的比較多,但當
> 採用動態載入時如果將所有的都載入時,不見得佔的記憶體不見得會比較少。
> 但如果整個系統有上百隻程式,而每次執行時可能只會用到部分的程式時,在這
> 樣的情況下也許會比較省記憶體。

嗯,的確有這種情況。不過,我們先就動機來看看,當我想要將程式切割成數個獨立模組時,主要的原因是整個專案太過龐大了,如果全部的程式編譯成一支執行檔,不但載入時間長,而且還沒用到的功能就已經載入記憶體裡面,形成浪費,對我來說,使用 package 的動機之一就是要改善上述的缺點,所以一定不會在程式載入時就把所有模組都載進來(不然乾脆用靜態連結成一支執行檔),否則,沒享受到動態載入模組的好處,反而還得處理一堆共享變數及函式傳參數等問題,等於自己找麻煩。
因此在這個前提下,您所描述的情況不見得會發生,如同有人問過,專案何時應該連結 runtime package,答案視情況而定,如果你的視窗系統中只有一支 Delphi 5 的執行檔,那麼您也許會用靜態連結,否則把所有用到的 package 加加起來所佔的磁碟空間一定大於一支執行檔的 size,佔用的記憶體可能也較多,相反地,當系統中安裝多個 Delphi AP 時,動態連結應該會是答案。而在我實際撰寫的專案中,也幾乎全部都是使用 runtime package。

我相信 Package 的設計的確能夠讓程式師開發出節省記憶體,並且提昇載入速度的程式,但好的工具不一定能造就出好的產品,工具的運用是否得宜,存乎一心。
那麼,我想應該將上面那句加上但書,改成這樣:

- 如果運用得宜,可以節省記憶體,並且提昇程式載入的速度。

> >...似乎沒有缺點:)
> 應該還是有,比方說架構就會比較複雜,公用變數的處理,大量參數
> 傳遞,物件的共用,MDI 較不容易做等。這些應該可以算是缺點吧 : P

算 :)
真的是有得必有失。

> 我個人倒是比較偏向使用 DLL 的方式,使用 Package 的話,那
> 只能做到切割程式時所有程式都是使用同一套開發軟體,但使用 DLL 就比
> 較沒有這樣的限制,就像 WinAMP 可以提供一些介面外掛其他人所寫的東西
> 一樣。

的確,但由於我所撰寫的大都是資料庫應用程式,程式要切成模組時仍會有一定的(比工具函式庫高的)相依性,所以在不易為其他程式語言使用的情況下,傾向使用 package 似乎是必然的結果?

> 我不熟 Linux,但如果要做到處理共用變數,其實 DLL
> 不見得要用到像 memory-mapped file 那麼複雜的方式才有辦法做到,
> 可以利用像 RTTI 或是一些繼承的技巧,也是可以很容易的做到的。
> 個人一點淺見,還望大家多多給予指教....
>
我倒把 RTTI 給忘了,待會試試能否用 RTTI 解決掉手邊的一些問題 :)

Regards,
Michael Tsai

3-27-2000, James Fu:

Dear Michael :
> - 如果運用得宜,可以節省記憶體,並且提昇程式載入的速度。

好久沒有一封信打那麼多字了,跟您討論真的很愉快,也許我們可以考慮把我們所談的這部分再加上一些 Sampe Code 或是 Demo 程式之類的,公開在網路上。

> 的確,但由於我所撰寫的大都是資料庫應用程式,程式要切成模組時仍會有
> 一定的(比工具函式庫高的)相依性,所以在不易為其他程式語言使用的情
> 況下,傾向使用 package 似乎是必然的結果?

我個人也幾乎都是寫資料庫應用程式(看來我們搞不好是同行 : P ),
因為這類的程式有時會因不同的客戶加減一些程式或是客戶要自行撰寫一些特殊的各案程式,為了怕有些客戶不會用 Delphi ,或是他們想要用 C++ 之類的,所以當初我個人在決定時,就會比較偏向 DLL。但是寫了不少程式下來,由於個案程式通常和系統的相依性很高( 比方說要共用 Database 的 Session 之類的),所以幾乎都是使用同版本的 Delphi 來開發,在這樣的情況下似乎和使用 Package 沒什麼大不了了,所以目前還是使用 DLL 的原因只是 "希望"提供讓其他的 Language 寫的程式一併可以掛進來。

James Fu

3-27-2000, Maxwell Lin 與 Scott:

Maxwell:
昨天看了大家的對話,才知道自己文不對題。哈哈,人都是會有先入為主的想法的。
還是做一些補充:關於昨天所提到的那些方法與實作,其實是OO pascal的直接運用。
比如說

在TBaseForm中,我們定義基本的form,然後給TBrowserEditForm,TMasterDetailForm等來繼承,於是當TBaseForm有所更動時,TBrowserEditForm程式碼重編譯即可完成相關的修改。

不過,寫這些程式碼時有些有趣的工作要做:由於Delphi好像沒有直接的操作介面讓您選擇TBaseForm,所以需要一些手工coding。以前我也用同樣的方式運作,但是後來覺得對公司並沒有直接明顯的幫助,就沒有堅持這種作法。(但對於一些不可見元件的設計與使用,我就用蠻多繼承的功能。)

Scott:
事實上,可使用Repository的方式,就可以將TBaseForm存入一旦要使用時,直接用File->New即可直接繼承取用,針對這個問題,事實上,作個Expert可能是個比較方便的做法。

Maxwell:
用DLL與Package其實是另外一個重要的議題,基本上他牽涉到作業系統如何管理程式,而不是單純的程式碼。我覺得如果連DLL都要有繼承的功能,可能是很久以後的事。我偏向com+元件 + MTS的運作模式。利用MTS管理com+元件,對公司日常的資訊工作分工有明顯的幫助。

Scott:
針對這個問題來說,最主要的問題在於標準規格的不確定性, 之前CORBA叫的震天價響, 後來COM出來之後,叫的人就少了,而DCOM現在又叫的最大聲,一次又一次的規格變化(或Enhance),讓追新技術變成是件風險很大的事。

Maxwell:
對於公司運作其實是有這樣的流程『分析->架構->實作->管理』,分析工作如何轉化成實作?!文件如何撰寫?!程式碼又如何利用繼承來管理?這些工作最好都能夠標準化。這樣對於日常工作才能可長可久。

Scott:
深有同感,如果是資料庫應用程式,Table的規格,相互的勾稽等等的東西,如DFD,Data Dictionary之類的東西可能比程式更重要。

就我個人的想法來說是比較偏向使用DLL以及程式中的繼承來配合,原因在於Package可能因Delphi的改版而變動(Delphi3是*.dpl,4,5是*.bpl)因而需要重Build,風險性太大,尤其是在資訊人員流動性如此高的現實狀況下,改版幾乎等於重寫,而DLL雖然寫作上比較麻煩,但以後維護上絕對是比較方便,由於DLL幾乎是個標準,只要遵守一定的準則,並不會因為Delphi改版就不能用而要重新Build。

配合DLL的使用,RTTI似乎也是個解決方式,但在每本夠份量的Delphi書中都再三提醒RTTI變動的可能性,不過,包進DLL應該沒有這個問題。

至於共用DLL的問題,事實上,根據大多數人的經驗是不太可能的。原因是自己沒辦法控制,原因在於DLL內部函式處理的動作並無法透明化,雖然在OO 強調封裝,但一旦作到比較細項的控制時,程式的可控制性是十分重要的。

個人的經驗是在自己Debug半天之後發現自己的程式沒有問題,而發現問題可能出在其他的問題(如API,Program,Driver......) 等等,為了穩定性,似乎除了自己重寫來控制資料以外,也沒有另外的解。

寫程式寫到後來,我幾乎不太敢使用別人寫的元件或是非標準元件,當然這已經是題外話了......。

http://www.geocities.com/huanlin_tsai/