Adapter 樣式

作者:蔡煥麟
日期:Feb-25-2002

摘要:從使用外來元件的風險以及評選原則,到利用 Adapter 樣式降低未來的風險,本文以實例說明如何以 Delphi 實作 Adapter 樣式以提昇程式的可維護性,以及如何搭配運用 Abstract Factory 樣式來設計動態的應用程式(在執行時期改變應用程式的行為)。

簡介

Adapter 是一種介面轉換的裝置,它的另一個稱呼是 wrapper(包裝者),而根據 [GHJV95] 的定義,Adapter 樣式的目的是:

「將類別的介面轉換成外界所預期的另一種介面,讓原先囿於介面不相容問題而無法協力合作的類別能夠都在一起用。」(取自 Design Patterns 中文版:物件導向設計模式)

在這裡篇文章裡面,我將以 Delphi 設計一個實用的 Adapter 類別,其主要目的並不在於讓介面不相容的類別能夠協同合作,而是將某個類別包裝成一個新的類別,讓這個類別的介面更符合我的需求。

在介紹這個類別之前,我們先來看一個有意思的問題,雖然跟主題好像沒什麼關係,實際上卻跟程式設計的活動息息相關,也跟使用 Adapter 的動機有一點關係。

發明 V.S. 研究

軟體開發人員必須經常面對各式各樣的需求,如果有一套開發工具提供了所有問題的解決方案是最好的,但這卻是不可能的,就拿 Delphi 來說,雖然它提供了非常多的 VCL 元件,但仍不足以應付專案的所有需求,此時程式設計師通常會採取兩種解決方式,一種是自己寫一個元件,可能是自己發明新的,也可能是參考或改寫別人寫好的元件;另一種則是直接使用現成的元件,不做任何的修改,這通常得花一番研究的功夫去了解如何正確地使用它。

兩種方式各有其使用時機,底限是:除非真的找不到你需要的元件,否則不要自己重新寫一個。因為市面上已經有許多現成的 VCL 元件任君挑選,花點時間去尋找比重新發明絕對來得省事多了。相信許多人都懂這層道理,但是包括我自己在內,有時候還是寧願自己寫一個而不願使用現成的元件。為什麼?

我相信每個程式設計師對於程式撰寫的風格以及命名方式都有各人的偏好,當這種偏好成為個人的習慣之後,對其他迥異的程式風格難免產生一些排斥,看不順眼,甚至覺得自己寫的一定比較好,因此就會傾向自行撰寫所有的程式碼來解決問題,這樣的心態是可以理解的。不只這樣,還有其它各種複雜的原因,例如:現成的元件並未完全符合自己的需求、以程式碼來展現自己的才華、發明創造的過程比較有趣,研究別人的程式碼比較乏味,而且可能還要忍受雜亂無章的程式碼、擔心以後維護的問題....等。

不可諱言,寫碼風格的差異是免不了的,但我相信些許的差異是可以接受的,寫碼標準的制定便在於希望能減少這些差異。剛才提的幾點因素裡面,大多與個人的性格有關,只有一個例外,也是最值得注意的,就是維護的考量。試想如果隨便找到一個元件就拿到自己的程式裡使用,那麼以後如果發現元件有臭虫的時候怎麼辦?原作者能夠在你滿意的時程內除去這個臭虫嗎?你自己親手除虫的話要花多少時間?所以,日後維護的問題也常令開發人員在考慮是否採用外來元件時猶豫不決。

以上就人們在遇到問題時經常捨棄現有的解決方案而寧願自己重新發明的現象大略提出幾點原因,如果結論是應該盡量使用現成的解決方案,那麼剩下的問題便是如何降低使用外來元件的風險了。

慎選外來元件

以下是我認為在選擇外來元件時應該考慮的條件:

如果能事先做一番調查評估,確認元件的品質符合一定的水準,我想一定能減少許多不必要的麻煩。

關於重新發明輪子的議題我想這樣的討論應該夠了,讓我們回到本文的主題,看看在開發應用程式時採用外來元件跟 Adapter 樣式有什麼關係。

動機

不知道你有沒有碰過這種情況,原本在應用程式中使用的元件,一段時間後要改用另一種元件,例如:原本使用 FastNet 的 NMFTP 元件來傳輸檔案,後來改用 Indy;原本使用 MSCOMM32.OCX 的通訊程式,後來要改用 Delphi 的 VCL 元件....諸如此類的情形在開發應用程式時多少都會遇到,請看看下面的例子。

數年前,我為客戶開發了一個資料庫應用程式,該程式是以 DBase 檔案來儲存資料,當時除了必須定期將資料檔案壓縮備份,還要有類似離線資料庫的功能,將部分資料匯出並壓縮至一片 1.44MB 的磁碟片上,方便客戶將資料帶在身上到處跑,他可以在家裡加班,或者帶著筆記型電腦在夏威夷的海灘上編輯資料,而修改後的資料也得壓縮在磁片上帶回到公司,經過解壓縮後匯入公司的資料庫中,因此我的程式得具備檔案壓縮及解壓縮的功能。

起初我撰寫 DOS 的批次檔來執行 pkzip.exe 及 pkunzip.exe 幫我壓縮檔案,批次檔寫起來毫不費力,而且大部分時候也運作正常,但是缺點就在於執行時會開啟一個 DOS 視窗,視覺的感受太差了,有時沒設定好的話,DOS 視窗還不會自動關閉,造成使用者的困擾,而且壓縮檔案的過程如果發生錯誤使用者也不易察覺。於是我開始考慮其他能夠跟 Delphi 視窗應用程式整合在一起的解決方案,一些商業元件例如 AbbreviaXceed ZipVclZip,..... 等都是一時之選,可是在節省成本的考量下,只好找找看有沒有免費又好用的元件。

Delphi Zip

在網際網路上可以找到的 VCL 壓縮元件還蠻多的,經過一番評選,最後我決定使用 Delphi Zip 這套壓縮元件,因為它不但符合開放原始碼的條件,而且功能不弱,可以製作跟 pkzip 完全相容的壓縮格式、自解檔、跨磁碟壓縮(span disk)、有線上輔助說明,並且支援多國語言。實際使用後也很令人滿意,程式在進行壓縮和解壓縮時可以顯示目前的進度,比起 DOS 視窗的文字模式自然美觀多了。這個應用程式一直都運作得很好,我也就不曾想過要改用其他的元件,直到最近開發的專案中又必須用到這套壓縮元件,才發現網站上公佈了一個令人遺憾的消息,負責維護 Delphi Zip 專案的 Chris Vleghert 因為癌症去世了,而且不知怎麼地連最新的 beta 版的程式碼也一併消失了,雖然早知道開放原始碼策略有其風險,這種情況卻是始料未及。於是,現在維護的工作又落回原作者 Eric Engler 的身上,他必須從現有的 beta 版本中挑出最新的來修改,這當然又得費一翻功夫了。

所幸到目前為止重建的工程都還進行得蠻順利,我大膽地將 beta 版使用在目前的專案中也沒發現什麼問題,但這件事卻讓我有所警覺:如果有一天這套壓縮元件無法適用在新的作業環境,或者臭虫一堆卻沒有人維護,到時候該怎麼辦?又或者有一天老闆心血來潮買了一套商業元件,或主管換人了,要求我的程式得改用另一套元件,那麼現有的程式要花多少功夫來修改?

於是,為了降低風險,免於日後維護可能造成的困擾,我決定未雨綢繆,試試 Adapter 樣式。

Delphi Zip 的網址是 http://www.geocities.com/SiliconValley/Network/2114/index.html

設計與實作

[GHJV95] 將 Adapter 的結構區分為兩種,一種是 class adapter,使用多重繼承的方式來轉換介面;另一種是 object adapter,主要是使用物件複合的技術。這裡使用的就是 object adapter 的方式,我先以抽象類別訂出符合自己需求的壓縮元件介面,然後以一個具像類別(concrete class)來實作該介面,而實作的部分大都是直接使用 Delphi Zip 元件的服務,參考圖一:

  (圖一)

TZipMaster 就是 Delphi Zip 元件中負責壓縮及解壓縮的類別,而 TZipAdapter 則實作了 TAbstractArchiver 定義的介面並且封裝了 TZipMaster。由於來自 Client 的要求大部分只是在背後轉呼叫 TZipMaster 的方法而已,可以想見實作 TZipAdapter 的程式碼不會很多,頂多也是加入一些轉換的處理而已。

由於我的應用程式只需要執行簡單的壓縮和解壓縮,因此一些額外的功能,像是製作自解檔和跨磁碟壓縮等相關的方法和屬性都不納入 TAbstractArchiver 裡面。為了省事,在 TAbstractArchiver 中定義的屬性和方法名稱都盡量跟 TZipMaster 一樣,像圖中的 Add 就是用來壓縮檔案的方法,取其「將檔案加入檔案櫃」之意,如果你覺得叫做 Compress 比較恰當,儘可使用自己喜歡的名稱,轉換不同的介面本來就是 Adapter 的用途之一。

這樣的設計至少有下列好處:

  1. 用戶端面對的介面變得更清爽簡潔,不像原本的 TZipMaster 有幾十個屬性和方法。新的介面十分易學易用。
  2. 如果以後要把 Delphi Zip 元件換掉,只要在 TZipAdapter 類別裡動手術而已,用戶端的程式完全不用修改。
  3. 如果要在應用程式中支援其他的壓縮格式,例如 RAR,ARJ,....等,只要再從 TAbstractArchiver 衍生新的類別即可,到時候除了 TZipAdapter,可能還會有 TRarAdapter,TArjAdapter....,但不管它們內部是怎麼實作的,應用程式的撰寫方式仍然不變(多型),因此應用程式可以輕易地支援多種壓縮格式,更甚者,再搭配個 Abstract Factory 就可以讓使用者在執行時期動態切換欲使用的壓縮格式。

接著就來看看這個壓縮介面及其實作,如果你覺得某些內容過於繁瑣,大可略過不看,有些細節只有對想要設計相同類別或者有用過 Delphi Zip 的人比較有幫助。

TAbstractArchiver

這個抽象類別必須包含基本的檔案壓縮以及解壓縮的功能,故命名為 TAbstractArchiver。類別的定義如下,簡潔起見,我把一些屬性的 Get/Set 方法給省略了:

type
  TAbstractArchiver = class(TObject)
  protected
    function GetArchiveName: string; virtual; abstract;
    procedure SetArchiveName(const Value: string); virtual; abstract;
    {....其他 Get/Set 方法 }
  public
    constructor Create; virtual;
    procedure Add; virtual; abstract;     // 將指定的多個檔案壓縮成一個檔案.
    procedure Extract; virtual; abstract; // 將指定的檔案解壓縮.

    property ArchiveName: string read GetArchiveName write SetArchiveName;
    property BaseDir: string read GetBaseDir write SetBaseDir;
    property SpecArgs: TStrings read GetSpecArgs;
    property SubFolders: Boolean read GetSubFolders write SetSubFolders;
    property CompressLevel: TCompressLevel read GetCompressLevel write SetCompressLevel;
  end;

其中 Add 和 Extract 分別是執行壓縮和解壓縮的動作,各個屬性的意義參見下表:

屬性名稱 說明
ArchiveName 壓縮檔的檔名。
BaseDir 在壓縮時作為相對路徑的基礎目錄名稱,而解壓縮時則是作為解開後的檔案所欲存放的路徑名稱。
SpecArgs 欲壓縮/解壓縮的檔案清單,可以明白列出檔案名稱,也可以使用萬用字元,例如:*.EXE。空字串表示所有的檔案都要處理。
SubFolders 是否連子目錄底下的所有檔案目錄一併壓縮。
CompressLevel 壓縮效率等級。

CompressLevel 的型態是 TCompressLevel,這是我另外定義的,因為 TZipMaster 一共提供了九種壓縮方式,我覺得不需要那麼多,而且如果以後的子類別要實作其他的壓縮格式,例如:RAR,ARJ...等也未必有那麼多種壓縮方式,因此將壓縮等級簡化成以下幾種:

  TCompressLevel = (clStore, clFastest, clFast, clNormal, clGood, clBest);

在實作後代類別時,只要在 GetCompressLevel 與 SetCompressLevel 方法中做一番對應轉換即可。

TZipAdapter

TZipAdapter 繼承自 TAbstractArchiver,並且封裝了 TZipMaster 類別,其類別定義如下:

type
  TZipAdapter = class(TAbstractArchiver)
  private
    FZip: TZipMaster;
    FBaseDir: string;
    FSubFolders: Boolean;
  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;

類別的建構元建立了 TZipMaster 物件,並且設定一些預設的屬性,而解構元則負責釋放 TZipMaster 物件:

constructor TZipAdapter.Create;
begin
  inherited;
  FZip := TZipMaster.Create(nil);  // 建立 Adaptee 物件.
  // set default properties
  SetCompressLevel(clBest);
  SetSubFolders(True);
end;

destructor TZipAdapter.Destroy;
begin
  FreeAndNil(FZip);
  inherited;
end;

另外,由於 TZipMaster 將壓縮和解壓縮會用到的屬性分開,例如 RootDir 和 BaseDir,一個只用在壓縮,一個則用在解壓縮,而在 TAbstractArchiver 中只定義了一個 BaseDir 屬性,因此這個屬性必須同時用在壓縮和解壓縮,其個別的作用在前面已經說過,這裡要說明的是實作 SetBaseDir 方法時必須同時設定 TZipMaster 的 RooDir 和 BaseDir 屬性:

procedure TZipAdapter.SetBaseDir(const Value: string);
begin
  FBaseDir := Value;
  FZip.RootDir := Value;
  FZip.ExtrBaseDir := Value;
end;

同樣的,TZipMaster 的 AddOptions 和 ExtrOptions 屬性也有相同功能的選項,分別是 AddDirNames 和 ExtrDirNames 選項,而在 TAbstractArchiver 中只定義一個 SubFolders 屬性來代表,所以 SetSubFolders 也要做些額外的處理:

procedure TZipAdapter.SetSubFolders(const Value: Boolean);
begin
  FSubFolders := Value;
  if FSubFolders then
  begin
    FZip.AddOptions := FZip.AddOptions + [AddDirNames, AddRecurseDirs];
    FZip.ExtrOptions := FZip.ExtrOptions + [ExtrDirNames];
  end
  else begin
    FZip.AddOptions := FZip.AddOptions - [AddDirNames, AddRecurseDirs];
    FZip.ExtrOptions := FZip.ExtrOptions - [ExtrDirNames];
  end;
end;

壓縮與解壓縮的方法很簡單,只是呼叫 TZipMaster 對應的方法而已:

procedure TZipAdapter.Add;
begin
  FZip.Add;
end;

procedure TZipAdapter.Extract;
begin
  FZip.Extract;
end;

為了讓 Delphi Zip 顯示中文訊息,還必須連結繁體中文訊息資源檔:

{$R ZipMsgTW.res}

如果新版的 Delphi Zip 沒有附上最新的繁體中文訊息檔案,也可以到我的網站下載,網址是 http://www.geocities.com/huanlin_tsai/

其他的程式碼都很簡單,這裡就不列出來,有興趣的話可以下載範例程式回去看。

屬性的 Get/Set 方法在類別裡定義好以後,可以按 Ctrl+Shift+C 讓 IDE 幫你產生各個方法的實作程式碼,像這樣:
function TZipAdapter.SetBaseDir(const Value: string);
begin
  inherited;

end;

但是要注意由於在父代類別中,這些方法都沒有實作,所以你得自行將 IDE 幫你產生的 inherited 這行去掉,否則執行時便會發生 'Abstract error'。

非視覺化程式設計

原本的 TZipMaster 繼承自 TComponent,並且註冊到 Delphi IDE,使用時可以從元件盤上拖一個 TZipMaster 元件到 form 上面就可以方便地設定各項屬性,然後呼叫 TZipMaster.Add 方法就可以執行壓縮檔案的動作。現在改用 TZipAdapter 之後,一切得以程式來完成,參考列表 1:

列表 1
procedure TForm1.btnCompressClick(Sender: TObject);
var
  arch: TArchiver;
begin
  arch := TZipAdapter.Create;
  try
    with arch do
    begin
      BaseDir := 'C:\Projects';
      SpecArgs.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 物件並不是在單元的初始化階段就建立好,而是使用延遲建構的技巧,當外界需要用到的時候才建立起來。

到目前為止,我們一共完成了以下類別:

現在,外界就可以透過字串參數令物件工廠建立對應的壓縮物件了,換句話說,使用者就可以在執行時期動態切換壓縮格式了。請參考列表 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 目錄下。

參考資料


讀者回應

> 物件工廠如果改用 Builder ( 建構者樣式 )  來作又是如何﹖
> 一樣使用延遲建構物件的方式,兩者利弊如何﹖

雖然兩者同樣歸類於 Creational Patterns,作用也相似,但是以這份文件中的範例程式來說,Builder 並不適合用來取代 Factory。因為:

總之,如果要建立複合物件的話,可以用 Builder 樣式,而這篇文章裡面的 Factory 的目的在於動態切換壓縮格式,只需建立單一物件,因此用 Factory 比較適合。