| pecArgs.Add(''''*.pas'''');
ArchiveName := ''''C:\test.zip'''';
SubFolders := True;
Add;
end;
arch.Free;
except
arch.Free;
raise;
end;
end;
這個簡單的範例會將 C:\Projects\ 目錄下的所有延伸檔名為 .pas 的檔案連同子目錄一併壓縮成 C:\test.zip。看起來似乎沒有原先視覺化的方式便利,但也說不上麻煩,因為我們的 TAbstractArchiver 定義的介面是為自己量身訂做的,簡潔易懂,況且要設定的屬性也不多。
拖放元件到 from 上面的開發方式是 RAD 工具的特色之一,很方便,但也有一些缺點。比如說,假設有一天你的同事要在他的電腦上修改你的程式,如果沒有事先安裝好必要的元件,專案開啟時就會出現一堆錯誤訊息告訴你類別找不到(Class TXxx not found),問你要 Ignore 還是 Cancel,如果你不小心按到 Ignore 鈕,這個元件就被刪掉了。若以非視覺化的撰寫方式,也就是以程式建立元件的方式,則頂多是編譯失敗,只要將元件所在的路徑加入函式庫搜尋路徑就行了,即使不安裝元件也沒問題。
現在假設一個更糟糕的情況,由於某種不可抗力的因素迫使你得改用另一套功能類似的元件,如果該元件在程式裡只用到一兩次還好,否則你恐怕得上窮碧落下黃泉把所有程式碼搜尋一遍,將 form 上面的元件逐一替換成新的元件(修改 .DFM 檔案),然後修改跟新元件不相容的程式碼,再重新測試程式。這工作既瑣碎又費時,能避免還是盡量避免吧,運用 Adapter 樣式就可以減少這類麻煩,以本文的範例程式來說,以後如果真要替換壓縮元件,只要修改 TZipAdapter 類別的程式碼就行了。
動態的應用程式
前面有提到,如果搭配一個 Abstract Factory 就可以讓應用程式在執行時期動態地切換多種壓縮格式,在我的上一篇文章「DLL 應用 - 設計可抽換的模組」裡面也提示可以用 Abstract Factory 來改進原有的設計,但是並沒有詳細說明,這一次我們就來看看如何實作這個專門用來生產物件的 Factory 類別。
我打算在程式中支援另一種壓縮格式:RAR,提供這項服務的類別命名為 TRarAdapter。而用來建立壓縮物件的 Factory 類別則命名為 TArchiverFactory,所以先前圖一的結構就變成這樣:
(圖二)
從圖中可以看出來新加入的 TRarAdapter 並沒有像 TZipAdapter 一樣封裝另一個類別,因為我打算直接用 DOS 版的 RAR.EXE 來幫我處理壓縮和解壓縮的功能,這麼做純粹是為了節省我撰寫範例程式碼的時間,並沒有其他特殊原因。最後再介紹 TRarAdapter 的實作,先來看看如何撰寫 Abstract Factory。
TArchiverFactory
首先我們必須提供一個類別註冊的機制,讓用戶端程式可以用字串來指定欲實體化的類別(參考圖二),因此用戶端程式只需面對抽象類別,並且可以在執行時透過字串來指定欲實體化的具象類別。顯然這個註冊機制必須能處理字串與類別的對應,我們用一個 TArchiverClassMapping 類別來提供這項服務,它會記錄一個類別的名稱與其對應的類別參考型態,參考列表 2:
列表 2
type
// Class reference type
TArchiverClass = class of TAbstractArchiver;
TArchiverClassMapping = class(TObject)
private
FMappingName: string;
FArchiverClass: TArchiverClass;
public
constructor Create(const AMappingName: string; AClass: TArchiverClass);
property MappingName: string read FMappingName;
property ArchiverClass: TArchiverClass read FArchiverClass;
end;
implemetation
constructor TArchiverClassMapping.Create(const AMappingName: string;
AClass: TArchiverClass);
begin
FMappingName := AMappingName;
FArchiverClass := AClass;
end;
接著是用來建立物件的工廠:TArchiverFactory,它封裝了 TObjectList,並且用它來維護一個「字串-類別參考」串列,如列表 3:
列表 3
unit AbsArchiver;
interface
uses
Windows, SysUtils, Classes, Contnrs;
type
TArchiverFactory = class(TObject)
private
FClasses: TObjectList; // Stores class-mapping list
protected
public
constructor Create;
destructor Destroy; override;
procedure RegisterClass(AClass: TArchiverClass);
function CreateInstance(const AClassName: string): TAbstractArchiver; overload;
end;
implemetation
constructor TArchiverFactory.Create;
begin
FClasses := TObjectList.Create;
end;
destructor TArchiverFactory.Destroy;
begin
FClasses.Free;
inherited;
end;
procedure TArchiverFactory.RegisterClass(AClass: TArchiverClass);
var
i: integer;
s1, s2: string;
begin
s1 := AClass.ClassName;
for i := 0 to FClasses.Count-1 do
begin
s2 := TArchiverClassMapping(FClasses.Items[i]).MappingName;
if SameText(s1, s2) then // 如果類別已經註冊.
Exit; // 就直接返回.
end;
FClasses.Add(TArchiverClassMapping.Create(s1, AClass));
end;
// Create an instance of TAbstractArchiver descendent
function TArchiverFactory.CreateInstance(
const AClassName: string): TAbstractArchiver;
var
i: integer;
acm: TArchiverClassMapping;
begin
for i := 0 to FClasses.Count - 1 do
begin
acm := TArchiverClassMapping(FClasses.Items[i]);
if SameText(acm.MappingName, AClassName) then
begin
Result := acm.ArchiverClass.Create;
Exit;
end;
end;
raise Exception.CreateFmt(''''<%s> 類別未註冊'''', [AClassName]) ;
end;
TRarArchiver
這次的主角是 TArchiverFactory,所以 TRarArchiver 就一切從簡,我只實作了壓縮的方法,而且是直接呼叫 RAR.EXE 幫我做這件事,參考列表 4。
列表 4
type
TRarAdapter = class(TAbstractArchiver)
private
FArchiveName: string;
FBaseDir: string;
FSubFolders: Boolean;
FSpecArgs: TStrings;
protected
function GetArchiveName: string; override;
procedure SetArchiveName(const Value: string); override;
{....其他 Get/Set 方法 }
public
constructor Create; override;
destructor Destroy; override;
procedure Add; override;
procedure Extract; override;
end;
implementation
{ TRarAdapter }
constructor TRarAdapter.Create;
begin
FSpecArgs := TStringList.Create;
end;
destructor TRarAdapter.Destroy;
begin
FSpecArgs.Free;
inherited;
end;
// 呼叫 DOS 版的 RAR.EXE 執行壓縮
procedure TRarAdapter.Add;
var
sSwitches: string;
sFiles: string;
sCmdLine: string;
i: integer;
begin
// 建立命令列所需的參數.
sSwitches := ''''-y ''''; // Yes on all queries
if FSubFolders then
sSwitches := sSwitches + ''''-r ''''; // Recurse subdirs
if FBaseDir <> '''''''' then
sSwitches := sSwitches + ''''-w'''' + FBaseDir + '''' ''''; // Work directory
sFiles := '''' '''';
for i := 0 to FSpecArgs.Count-1 do
sFiles := sFiles + FSpecArgs[i] + '''' '''';
sCmdLine := ''''RAR a '''' + sSwitches + ArchiveName + sFiles;
WinExec(PChar(sCmdLine), SW_NORMAL);
end;
procedure TRarAdapter.Extract;
begin
inherited;
ShowMessage(''''TRarAdapter.Extract not implemented yet!'''');
end;
向物件工廠註冊
別忘了 TZipAdapter 和 TRarAdapter 都要向物件工廠 TArchiverFactory 註冊,這樣用戶端才能透過字串參數令物件工廠建立對應的物件,你可以在單元的 initialization 區段中進行註冊: unit ZipAdapter;
initialization
ArchiverFactory.RegisterClass(TZipAdapter);
==================== unit RarAdapter;
initialization
ArchiverFactory.RegisterClass(TRarAdapter);
其中 ArchiverFactory 是一個事先建立好的 Factory 物件,因為整個應用程式當中用來生產壓縮物件的工廠只需要一個就夠了,所以可以用簡單的 Singleton 樣式解決,參考列表 5。
列表 5
// 取自 AbsArchiver.pas 單元
interface
function ArchiverFactory: TArchiverFactory;
implementation
var
g_ArchiverFactory: TArchiverFactory = nil;
function ArchiverFactory: TArchiverFactory;
begin
if g_ArchiverFactory = nil then
g_ArchiverFactory := TArchiverFactory.Create;
Result := g_ArchiverFactory;
end;
initialization
finalization
if g_ArchiverFactory <> nil then
FreeAndNil(g_ArchiverFactory);
這個唯一的 Factory 物件並不是在單元的初始化階段就建立好,而是使用延遲建構的技巧,當外界需要用到的時候才建立起來。
到目前為止,我們一共完成了以下類別:
- 抽象的壓縮/解壓縮類別 TAbstractArchiver(AbsArchiver.pas)。
- 封裝 Delphi Zip 壓縮元件的TZipAdapter 類別(ZipAdapter.pas)。
- 提供 RAR 壓縮功能的 TRarAdpater 類別(RarAdapter.pas)。
- 提供字串與類別對應的 TArchiverClassMapping 類別(AbsArchiver.pas)。
- 用來生產壓縮物件的物件工廠 TArchiverFactory 類別(AbsArchiver.pas)。
現在,外界就可以透過字串參數令物件工廠建立對應的壓縮物件了,換句話說,使用者就可以在執行時期動態切換壓縮格式了。請參考列表 6 的程式寫法,並且和先前的列表 1 的寫法對照一下有何不同。
列表 6
procedure TForm1.btnCompressClick(Sender: TObject);
var
arch: TAbstractArchiver;
sClassName: string;
begin
// Create concrete archiver class depending on user''''s choice
if rdoZip.Checked then
sClassName := ''''TZipAdapter'''';
if rdoRar.Checked then
sClassName := ''''TRarAdapter'''';
arch := ArchiverFactory.CreateInstance(sClassName);
try
with arch do
begin
SpecArgs.Text := edSpecArgs.Text;
BaseDir := edBaseDir.Text;
ArchiveName := edArchiveName.Text;
SubFolders := chkSubFolders.Checked;
Add;
end;
arch.Free;
ShowMessage(''''Done.'''');
except
arch.Free;
raise;
end;
end;
程式的執行畫面如下:
結語
本文首先探討了重新發明輪子的老問題以及使用外來元件要注意哪些事項,然後以實際的例子來說明外來元件可能引發的風險,以及運用 Adapter 樣式來解決類似的問題,最後則搭配 Abstract Factory 樣式讓應用程式能夠處理動態的需求。為了方便說明,文中使用的範例程式都簡化過了,也許對多數人來說不那麼有用,但重要的是設計這些類別的用意在於:
- 介面轉換。
- 建立容易維護的應用程式。
- 建立動態的應用程式。
Design patterns 提供一些好處,但不保證你能得到這些好處,如果運用得當的話,我相信對於程式的品質和可維護性的提昇必然大有幫助,希望這篇文章能讓你對 Adapter 樣式有一番初步的認識。同時也別忘了,唯有透過實作練習才能確切掌握 design patterns 的精髓,並成為你解決問題的利器,祝各位學習愉快。
範例程式
按這裡下載範例程式:AdapterDemo.zip
壓縮檔解開後會產生兩個目錄,其中 AdapterDemo1 是只有 TZipAdapter 的版本,而 AdaptrerDemo2 則是可以動態切換壓縮格式的版本。由於 AdapterDemo2 需要 DOS 版的 RAR.EXE,所以壓縮檔裡面也順便附上這個工具程式。
範例程式是以 Delphi 5 撰寫,如欲編譯這兩個範例程式,你還必須先安裝 Delphi Zip 元件,你可以到這裡下載:http://www.geocities.com/SiliconValley/Network/2114/index.html。安裝後記得要將 Zip.DLL 和 Unzip.DLL 這兩個檔案複製到 Windows 的 System 目錄下。
參考資料
|