在實作 package 時,我遇到了一些問題,承蒙DelphiChat多位熱心朋友的幫忙解決小弟許多疑惑,因此當傅兄(James Fu)提議將討論信件整理出來並配合範例供大家參考時,我也很贊同,並且將平常的筆記及範例程式稍作整理一番,於是有了這份文件。
如果你正在考慮要不要將 package 實際運應用在專案裡面,你可以先看看 package 的優點及缺點。想要進一步了解package
如何使用,建議您先閱讀參考資料裡面的一些文章,然後下載範例自己動手做看看。
市面上已經有很多現成的資源,包括書籍,雜誌,網際網路上面都有介紹 package 的基礎知識,所以我並不想在這裡重複這些東西,我將這些資源列出來供需要的人參考:
- Delphi Package 無痛使用 by
Deven Tzu。 - Delphi 學習筆記 (Win32 基礎篇) by 錢達智,碁峰出版。
- Object Packages Enable Highly Modular Applications by James Heyworth
Presented to the Inprise Asia-Pacific Conference, 10-13-1998 - The Builder Pattern
Extending Frameworks - Building Add-in Packages
by Xavier Pacheco - Dynamically loaded package demo
PkgDemo.zip (Created 10/29/97, Size 13,217 bytes)
This application and associated package demonstrates how you can use the packages feature in Delphi 3 to partition your applications. The example illustrates loading a package and instantiating a form class from the package. Additionally, it provides tips on
successfully unloading packages. - 在 Dlephi 5 的線上說明中輸入 "package support routines", 可以找到相關函式。
以上所列的資源應該相當夠了,另外我也用 Delphi 5 寫了三個範例,分別是:
包含一個主程式及兩個 package ,壓縮檔內含子目錄,若以 Pkunzip 解壓縮時請加 -d 參數。其中ProjectGroup1.bpg 可以建立所有的模組。
PkgDemo1 的改進版本,壓縮檔內含子目錄,若以 Pkunzip 解壓縮時請加 -d 參數,解開後請先閱讀其中的 Readme.txt,裡頭有簡單的說明。
從這個範例應該可以衍生出更複雜的實際應用,如果有不了解之處, 我建議您先看看 James Heyworth 的那篇文章(見上方參考文獻)及其範例。
此範例將 DataModule 也做成一個獨立的 package,請參考 _readme.txt 的說明。
以下是網友提供的範例:
示範如何將 DataModule 放在 package 中讓其他程式共用。
以下是我的筆記,比較片段而缺乏完整性,僅供參考:)
- 應用程式可以被高度的模組化,而且可以逐漸交付完成的功能給客戶。
- 開發人員可隨時因應市場策略包裝成多樣性的軟體。
- 維護比較方便。
- 節省記憶體,並且提昇程式載入的速度....如果使用得宜的話:)。
在將模組獨立出來的過程中會遇到共享全域變數及物件等問題,需要花比較多的心思在模組化的設計上面,關於這些問題,可以參考
DelphiChat 討論信件精華 或許能得到些線索。
在撰寫幾個 package 的測試程式之後,我還是沒有將 package 應用在實際的專案開發中,而仍然使用 DLL,其最主要的原因,正是 package 優於 DLL 之處--可以共享變數。這項功能的立意很好,但也帶來了另一些限制,主要是名稱衝突的問題,使得共用的 unit 一定要放在 package 裡面,否則當兩個 package 包含了相同的 unit,其中一個就無法載入,我們覺得這會造成麻煩。另外,由於其他的小組成員對於 package 的使用不熟,容易出 trouble(例如:project
要加入 .dcp 之類的),這也是考量之一。
動態載入 package 的函式(SysUtils.pas)
- LoadPackage
載入 package 至應用程式的執行空間。
函式原型:function LoadPackage(const Name: string): HMODULE;參數 Name 為欲載入的 package 的完整路徑及檔案名稱。
傳回值為載入的 package 的 Handle,此 Handle 在釋放 package 時會用到,所以必須好好保存。此函式會先檢查欲載入的 package 所包含的 units 是否已在目前載入的 package 中重複出現,若程式單元重複就不載入且顯示錯誤訊息,否則會將此 package 中所包含的各單元的 Initialization 區段的程式碼執行一遍。請注意,LoadPackage 會先檢查該模組是否已經載入了,如果是,就直接傳回該模組的Handle,並且將模組的載入計數加一(increase reference count),模組中各單元的Initialization 區段不會被執行。所以即使重複的呼叫此函式,我們並不會在程式的執行空間中擁有多個相同的模組。
- UnloadPackage
函式原型:procedure UnloadPackage(Module: HMODULE);
此函式會先將指定的 package 模組所包含的各單元的 Finalization 區段的程式碼執行一遍,然後卸載該模組。
從 VCL 的原始碼 SysUtils.pas 中挖掘,可以發現它此函式依序呼叫了 FinalizePackage 及 FreeLibrary,其中 FinalizePackage 會去執行 Finalization 區段的程式碼,而 FreeLibrary 是 WinAPI,它會先遞減模組的參考計數,如果參考計數為零才會將模組釋放掉。
VCL 類別註冊函式 (Classes.pas)
- RegisterClass 與 RegisterClasses
procedure RegisterClass(AClass: TPersistentClass); procedure RegisterClasses(AClasses: array of TPersistentClass);
透過串流系統(stream system)註冊類別,已註冊過的類別不會重複註冊,但如果是不同 package 有相同的類別名稱,則呼叫此函式會出現 EFilerError 的錯誤訊息。此函式通常放在程式的 initialization 區段。
- GetClass
透過 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 函式。
- 載入/卸載 package 的過程
1. 載入並初始化 package。
2. 使用 package。
3. 資源回收(釋放你從 package 中建立的物件)。
4. 註銷類別。
5. 卸載 package 。
注意事項及建議
- 各個 packages 所包含的程式單元名稱不能相同,而且各個 packages 利用 RegisterClass 所註冊的類別名稱必須為唯一。所以在為你的 package 命名的時候,要注意名稱可能重複的問題。舉例說明:
例一:Unit1.pas 是一個獨立功能的程式單元,由於必須經常使用到,為了方便起見,就把 Unit1.pas 加入到 PackageA 及 PackageB 中,這兩個 package 都能正確編譯連結,但只要其中一個 package 被載入之後,另一個就無法載入。
例二:PackageA.pkg 包含Unit1.pas,而 Unit1 會註冊 THelloForm 類別,PackageB.pkg 包含Unit2.pas,而 Unit2 也註冊 THelloForm 類別,這兩個 package 都能正確編譯連結,但只要其中一個 package 被載入之後,由於 THelloForm 已經註冊過,所以另一個package 在載入時就無法註冊 THelloForm 而產生錯誤。
- 同一個 package 載入多次並不會發生錯誤,因為 RegisterClass 對於同一個已註冊過的類別,是不做任何事而直接返回的。但是,如果不同 package 中有相同的類別名稱,則再次呼叫 RegisterClass 時會出現 EFilerError 的錯誤訊息。
- Package 專案選項
- 使用 Runtime package, Explicit rebuild。
- Output directory 建議設定為該專案目錄下的 Bin 目錄,或者在專案目錄下建立一個 Packages 目錄,然後在這個 Packages 目錄下存放各個 package 專案,而各個專案的 Output directory 全部設定為 Bin 目錄,其路徑類似這樣:Projects\MyProject\Packages\Bin,而 DCP output directory 則指定為目前的目錄,也就是 "."(不包含引號),若不指定的話,Delphi5 會將產生的 .DCP 檔案輸出至Delphi5\Projects\Bpl\
目錄下。
討論主題:應用程式模組切割問題
參與人員:Fang Lin, Hippy, James Fu, MR7, Maxwell Lin, Michael Tsai, Scott
這些討論信件是依照日期的先後順序排列,由於信件中引言所佔的比例蠻大的,所以我做了些刪改,目的是為了更易於閱讀,若有不當之處尚請見諒。
3-10-2000, Michael Tsai:
Dear all: Michael Tsai |
3-10-2000, Fang Lin:
Hi: BasePackage 表示 TBASEDATAFORM所包成的package如果要視覺化繼承Form, 必須在Project Group中將BasePackage 加入,否則會出現 TBaseDataForm not found的錯誤訊息. 不知道有沒有更好的方法? Fang Lin |
3-11-2000, Michael Tsai:
Hello, Fang: 1. New 一個標準的 Form,假設類別名稱為 TForm1。 雖然不完美,但總算有法子解決在 package 中視覺化繼承 Form 的問題了。如果要有個比較"乾淨"的解決方案,或許撰寫一個Form Expert 是最好的解決方式,關於這方面,Ray Lischner 有一本著作: Michael Tsai |
3-15-2000, Maxwell Lin:
TO Dear Michael: 至於畫面部分,就依據client來選。比如在lan上就用delphi做,在www上就用ASP + VBScript或ASP + JScript。 我經驗中:畫面共用性其實很低的,但這也只是我的想法啦,不一定是對的。 Maxwell Lin arus@tpts1.seed.net.tw |
3-15-2000, Michael Tsai:
Hello, Maxwell:
如果要說明我的整個設計,恐怕得花上好些篇幅,容我概述一二: Regards, |
3-16-2000, Hippy:
Hello, Hippy |
3-21-2000, MR7:
> 這幾天翻 Delphi 手冊時發現了一個編譯指示似乎跟Package有關,但是我沒試過 |
3-22-2000, James Fu:
Dear Friend : >您這樣的問題有點難說喔。因為能像您這樣架構程式的人不多, 插個花一下,我想切割成好幾個模組這樣的架構應該不少,個人認為,有時為了維護上的方便,比方說您今天開發一個應用系統給您的客戶,用了幾天之後發覺有一個Form上面打了一個錯字或是一個處理寫錯了,也許您可以把所有的程式重新Complier成一個新的exe檔傳給客戶,即使您使用了 D4 or D5 的 RunTime Package 的方式,我想一個系統可能還是需要好幾MB。 至於畫面共用性我個人倒是覺得在系統中應該會很高,比方說您的系統中有 10 隻左右的輸入資料作業,您不大可能這幾支作業新增、刪除、修改之類的按鈕都設計的不同大小、位置吧。當然啦,這樣的情況下可能會讓畫面有些呆板,但對使用者來說或許會比較容易一點學習呢? 小弟才疏學淺,將一些個人的淺見願與大家分享,還望大家不吝指教。 James Fu |
3-23-2000, Michael Tsai:
經過幾次的討論,我們知道撰寫獨立的模組的確有許多優點,茲整理如下: ...似乎沒有缺點:) 如果用 Delphi 及 BCB 開發的話,個人覺得還是用 Package 的方式切割會比較輕鬆,主要是因為在主程式與各模組之間很有可能需要傳遞參數,而這些參數可能是 VCL 物件,使用 DLL 的話會比較麻煩。另外,DLL 無法共用變數,而必須採用如 memory-mapped file 或其他方式來解決,也是要考慮的。 至於畫面共用,我贊同您的看法,事實上,我的程式大量使用視覺化的Form繼承關係,以確保操作介面的一致性。 Regards, |
3-23-2000, James Fu:
Dear Friend : >...似乎沒有缺點:) >如果用 Delphi 及 BCB 開發的話,個人覺得還是用 Package James Fu |
3-25-2000, Michael Tsai:
Hello, James: > > - 節省記憶體,並且提昇程式載入的速度。 嗯,的確有這種情況。不過,我們先就動機來看看,當我想要將程式切割成數個獨立模組時,主要的原因是整個專案太過龐大了,如果全部的程式編譯成一支執行檔,不但載入時間長,而且還沒用到的功能就已經載入記憶體裡面,形成浪費,對我來說,使用 package 的動機之一就是要改善上述的缺點,所以一定不會在程式載入時就把所有模組都載進來(不然乾脆用靜態連結成一支執行檔),否則,沒享受到動態載入模組的好處,反而還得處理一堆共享變數及函式傳參數等問題,等於自己找麻煩。 我相信 Package 的設計的確能夠讓程式師開發出節省記憶體,並且提昇載入速度的程式,但好的工具不一定能造就出好的產品,工具的運用是否得宜,存乎一心。 - 如果運用得宜,可以節省記憶體,並且提昇程式載入的速度。 > >...似乎沒有缺點:) 算 > 我個人倒是比較偏向使用 DLL 的方式,使用 Package 的話,那 的確,但由於我所撰寫的大都是資料庫應用程式,程式要切成模組時仍會有一定的(比工具函式庫高的)相依性,所以在不易為其他程式語言使用的情況下,傾向使用 package 似乎是必然的結果? > 我不熟 Linux,但如果要做到處理共用變數,其實 DLL Regards, |
3-27-2000, James Fu:
Dear Michael : 好久沒有一封信打那麼多字了,跟您討論真的很愉快,也許我們可以考慮把我們所談的這部分再加上一些 Sampe Code 或是 Demo 程式之類的,公開在網路上。 > 的確,但由於我所撰寫的大都是資料庫應用程式,程式要切成模組時仍會有 我個人也幾乎都是寫資料庫應用程式(看來我們搞不好是同行 : P ), James Fu |
3-27-2000, Maxwell Lin 與 Scott:
Maxwell: 在TBaseForm中,我們定義基本的form,然後給TBrowserEditForm,TMasterDetailForm等來繼承,於是當TBaseForm有所更動時,TBrowserEditForm程式碼重編譯即可完成相關的修改。 不過,寫這些程式碼時有些有趣的工作要做:由於Delphi好像沒有直接的操作介面讓您選擇TBaseForm,所以需要一些手工coding。以前我也用同樣的方式運作,但是後來覺得對公司並沒有直接明顯的幫助,就沒有堅持這種作法。(但對於一些不可見元件的設計與使用,我就用蠻多繼承的功能。) Scott: Maxwell: Scott: Maxwell: Scott: 就我個人的想法來說是比較偏向使用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......) 等等,為了穩定性,似乎除了自己重寫來控制資料以外,也沒有另外的解。 寫程式寫到後來,我幾乎不太敢使用別人寫的元件或是非標準元件,當然這已經是題外話了......。 |