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 視窗應用程式整合在一起的解決方案,一些商業元件例如 Abbrevia,Xceed Zip,VclZip,..... 等都是一時之選,可是在節省成本的考量下,只好找找看有沒有免費又好用的元件。
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 的用途之一。
這樣的設計至少有下列好處:
- 用戶端面對的介面變得更清爽簡潔,不像原本的 TZipMaster 有幾十個屬性和方法。新的介面十分易學易用。
- 如果以後要把 Delphi Zip 元件換掉,只要在 TZipAdapter 類別裡動手術而已,用戶端的程式完全不用修改。
- 如果要在應用程式中支援其他的壓縮格式,例如 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'''';
S[1] [2] [3] 下一页
没有相关教程