作者: 點空間 陳國生
Email: bruce123@edirect168.com
日期:2003/09/06
設計計算機系統(tǒng),最困難的部分是什么?也許你可以找出一大堆的答案,但真正最困難的部分是面對系統(tǒng)功能的改變,這是因為我們無法去預知這些改變。但或許你會說,那就盡量保持彈性,話雖如此,但我們?nèi)绾沃酪谑裁吹胤奖3謴椥??但如果你的系統(tǒng)無法去應付這些改變,那么就只有走向重寫一途,這無疑是一件讓人沮喪的事。在面對這個問題,我們`必須學會用設計模式去思考,如果系統(tǒng)的設計已經(jīng)僵化了,我們就讓它柔軟下來,如果柔軟不下來,就把它凍結(jié)起來,如果原來設計彈性就很好,我們就繼續(xù)擴充功能,沒有解決不了的事,以下我們舉個例子來說明這個想法。
首先,我們從一個設計模式說起:Facade 外觀模式。我們來談談這個模式,這個模式的用途是將一個子系統(tǒng)內(nèi)部的眾多對象統(tǒng)一起來,產(chǎn)生新的接口出來。這讓我想到我們中國有句成語叫:老干新枝。這個意義是,老干已經(jīng)失去了活力,但為了生存下去,必須產(chǎn)生新的枝芽,這跟一個已經(jīng)僵化的系統(tǒng)一樣,我們無法期望從原來的枝長出葉子,所以只好另長新芽。
如果一個系統(tǒng)已經(jīng)糾成一團,如左圖般的狀態(tài),再繼續(xù)擴充功能,或更動系統(tǒng)功能,那無疑的是雪上加霜,加速系統(tǒng)的走向死亡之路。由于系統(tǒng)內(nèi)部的對象交互影響,只要移動部分的功能,Client 端的使用接口勢必要跟著改變,繼續(xù)使用這樣的系統(tǒng)結(jié)構,終究有一天會土崩瓦解。重新去調(diào)整這個致命的缺點,又跟重新改寫沒有兩樣,常常這樣讓人陷入進退兩難的窘境。
如果我們可以用設計模式去思考解決問題,如右圖將原系統(tǒng)視為一個子系統(tǒng),使用Facade這個樣式,重新產(chǎn)生使用接口,將這混亂的局面控制,不再像癌癥一樣繼續(xù)擴散開來,內(nèi)部的調(diào)整不再影響Client 的使用接口,那么就可以讓系統(tǒng)取得一個擴充功能的契機。系統(tǒng)控充功能與內(nèi)部結(jié)構的改善,這是兩個不同的方向,千萬不要攪在一起,也不要同時去改變,這樣會使問題更加的復雜,
接下來,我們來探討一下什么叫重整?Martin Fowler 于書中定義它是一種「在不改變代碼外在行為的前提下對代碼做出修改,以改進代碼的內(nèi)部結(jié)構」的過程。如果說重整的目的不在于對既有的代碼增加新功能,而只是不影響程序既有功能及正確的執(zhí)行下,改善程序代碼。那么對許多人而言,讓他們感到相當疑惑 的是,這種行為是否能帶來任何的益處?重整如果只是改善內(nèi)部結(jié)構,那顯然是無助于增加效益,所以重整的目的勢必與往后的系統(tǒng)擴充功能產(chǎn)生緊密的關聯(lián)。俗語說:休息是為走更遠的路,所以沒有重整,也就沒有更遠的路。而本篇重點是在改善結(jié)構跟擴充功能這兩個想法緊密的結(jié)合在一起。也就是說在改變現(xiàn)有結(jié)構之后,進行系統(tǒng)功能擴充,根據(jù)現(xiàn)有的結(jié)構加以改善,以符合擴充的需要。試想,如果沒有新的需求產(chǎn)生,需要擴充功能,又何必大費周章的去進行重整呢?但是從本篇的范例中,有些范例雖然未擴展功能,但經(jīng)過使用樣式設計進行重整結(jié)構的改善,幾乎已經(jīng)可以看見擴展新的功能的曙光,這是重整帶來最大的好處。
俗語說:做衣容易,改衣難。這說明一件事,雖然做衣服跟改衣服,用的是同樣的技術,但卻是不同的思維,所以變成了會做衣服卻不一定會改衣服。對設計系統(tǒng)而言,系統(tǒng)中任何一個變動,就像改衣服一樣,學習克服這些變動,才能快速的反應變動。近年來軟件工程的思維傾向貼近客戶需求,擁抱改變,但這不光是口號而已,在這個思潮下,背后必須有強而有力的設計技術及思考模式所支撐,因此學習這些基本技術及不斷的磨練你的思考模式,才能去符合這樣的要求。本篇就是基于這樣的想法,將設計模式與重整兩個重要的基本技術加以結(jié)合運用,實現(xiàn)系統(tǒng)面對改變時,如何去反應變動,及如何讓系統(tǒng)保持適當?shù)膹椥浴?/p>
底下,對本篇所提供的實作范例,作簡單扼要的說明:
范例一:運用Observer 觀察者模式解開單元之間的耦合,是重整的典型范例,本范例著眼于系統(tǒng)結(jié)構改善,并未擴充新功能。
范例二:運用Template Method 樣式設計數(shù)據(jù)庫的異動交易控制,這個范例架構在原系統(tǒng)上面,將原來程序代碼加以凍結(jié),并擴展新功能。
范例三:運用Factory 工廠模式設計多人使用的權限控制,這個范例使用替代的方式,在不改變原有功能的前提增加了系統(tǒng)的彈性。
范例一
運用Observer 觀察者模式解開單元之間的耦合
作者:陳國生
Email: bruce123@edirect168.com
【解決問題】
在Delphi 的對象中,TForm 是一個很搶眼的角色,因為在一個Form 當中,我們可以輕易的結(jié)合許多不同的對象一起工作,幾乎是無所不能,但是在比較復雜的系統(tǒng)中,我們必須根據(jù)不同的功能,切割放在不同的Form 上面 ,或者說是在不同單元當中。當一個系統(tǒng)存在許多不同單元之后,我們要如何去讓這些單元可以相互合作?使用USES 可以將兩個不同單元視為一個單元來使用,但遺憾的是,這種設計方式往往會造成單元之間的耦合性過高,當一個單元變動之后,往往其它的單元也會跟著受影響,而整個系統(tǒng)也就因此牽一發(fā)而動全身了。
當然,我們可以透過使用接口讓這個問題不至于過度的惡化,但仍無法確實的解決這個致命的糾纏。因為透過使用接口來運作,仍然必須USES這個單元,當這個單元被取消之后,仍然會影響其它單元的運作。
【面臨問題】
也許,此刻你的程序已經(jīng)陷入這樣致命的糾纏當中,我們要如何才能去解開這個結(jié)呢?
【運用技術】
本范例將運用Observer 觀察者樣式,來解決單元之間的糾纏不清。首先我們先來看看這個觀察樣式定義:觀察者模式定義對象間的一對多的相互關系,使得一個對象改變時,其它相關對象皆可獲得通知并自動更新。這個模式里面,會有一個觀察者,及一個以上的被觀察者,當觀察者的狀態(tài)改變時,被觀察者被主動的告之,而觀察者負責通知,被觀察者卻不用知道觀察者是誰。
從理解一個抽象概念的,到運用在解決問題上面,過程通常不是那么的容易。根據(jù)觀察的樣式提供的概念,其實Delphi 這個RAD 已經(jīng)幫我們設計許多功能強大的對象,所以重要的是,我們?nèi)绾稳ミ\用這個概念去解決問題,要把焦點放在解決問題上面。
觀察者模式,有點類似Window里面的訊息傳遞機制,如果我們要實作這樣的機制,那么必須要有一個可以儲存這些訊息及事件的容器型對象。在Delphi 中有許多現(xiàn)成的對象,你無須費心的去設計這樣的對象,只要找出可以實踐這樣想法的對象即可,本范例中,我們運用TActionList來實現(xiàn),TActionList 是一個事件串行,現(xiàn)在就看我們?nèi)绾紊朴眠@個對象,去達到訊息傳遞的目的了。
在下面的范例中,我們將實作一個橫跨整個系統(tǒng)的觀察者樣式,解開單元之間的耦合問題。
【實作范例】
第一步,建立一個TDataModule,并在DataModule 1上面放置一個TActionList 的對象。
第二步,分別建立兩個TForm,F(xiàn)orm1及Form2 ,在Form1,F(xiàn)orm2 各放一個TEdit 以供測試。
第三步,在TDataModule 增加一個屬性,myValue:string。
第四步,在TActionLIst 增加一個TAction 對象,并寫入控制事件:
procedure TDataModule1.Action1Execute(Sender: TObject);
begin
if Form1<>nil then Form1.Edit1.text:=myValue;
end;
第五步,在Form2的Edit1的OnKeyPress 寫入:
procedure TForm2.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
DataModule 1.myValue:=Edit1.text;
DataModule 1.Action1.Execute;
end;
第六步,在Form1激活Form2,然后在Form2的Eedit1 輸入資料,此時可見Form1.Edit1 同步收到訊息了。
從這個范例中,可以將Form2視為是一個觀察者,而Form1 是一個被觀察者,當Form2的TEdit的值改變后,透過事件通知,改變了Form1的TEdit,然而Form2 并無USES Form1 ,也不知道Form1的存在,所以即使Form1消失了,仍然不會影響Form2的運作。
所以,從這個運作架構中,我們可以依系統(tǒng)變動的需求,不斷的增加控制事件,卻不會影響單元之間的關系。
【結(jié) 論】
在一個大的系統(tǒng)里面,控制單元之間的耦合性非常重要,以上述的范例來說,不論系統(tǒng)如何改變,各個單元都可以維持正常的運作,既相互合作又不會相互影響,其變動姓被有效的控制在一個事件串行當中,當單元變動時,我們很容易追蹤,且不會擴散到整個系統(tǒng)。
范例二
運用Template Method 樣式設計數(shù)據(jù)庫的異動交易控制
作者:陳國生
Email: bruce123@edirect168.com
【解決問題】
什么是異動交易控制?簡單的說,就是如何確保在資料改變的過程,可以順利的完成,異動交易控制最主要的著眼點在于對交易失敗的控制,試想一個交易,基于某種不可預期的原因造成程序產(chǎn)生錯誤,中途被迫終止交易,那之前已經(jīng)改變的資料算不算數(shù)?如果不算數(shù),那之前已經(jīng)交易的資料怎么辦呢?
舉例來說,一張進貨單可能要同時更新單據(jù)檔及庫存檔,如果中途失敗,可能造成單據(jù)文件有資料而庫存文件沒有更新,要如何才能避免這種嚴重的錯誤呢?那就是必須進行異動交易的控制,在一個異動交易控制中,只要有一筆交易失敗,那么之前所有交易的資料,將被還原至交易之前。
在Delphi 的Connection 對象已經(jīng)提供了良好的控制程序。以TAdoConnection為例,它的語法格式如下:
try
// 開始進行交易控制
adoconnection1.BeginTrans;
// 寫入您的交易程序
.......
.......
.......
// 將交易資料實際寫入數(shù)據(jù)庫
adoconnection1.CommitTrans;
except
// 如果發(fā)生意外,將之前所有已交易資料還原
adoconnection1.RollbackTrans;
end;
從上面的控制結(jié)構來看,它是將一個交易程序帶入交易控制之中,那么也就是說,如果你原先寫的交易程序如果沒有做這樣的控制,只要加入這樣的控制程序,就可以進行異動交易。但在程序重整的技術中,有一點必須要注意,那就是盡量不要去更動現(xiàn)有的程序,因為一旦更動程序,就必須重新作測試,因此在不更動現(xiàn)有的程序的前提下,我們要如何去修改程序,讓程序具有異動交易控制能力呢?
【面臨的問題】
雖然Delphi 已經(jīng)有了很好的解決方式,但是對一些已經(jīng)寫好的交易程序,我們在不變動原有的程序代碼,如何進行功能的擴充呢?
【使用技術】
這個案例,我們將利用到重整技術的概念及樣式設計來解決這個問題。范例中我們會用到Template Method 這個樣式來設計一個程序,什么叫Template Method ?它的定義如下:
在父類別中定義一個算法的骨架,但將一些步驟延遲到其子類別中執(zhí)行。套句話說,一個人犯罪是要坐牢的,這個犯罪要坐牢就是一個樣版,至于做幾年的牢,要看當時犯了什么罪才來決定。
這樣的說法還是很抽象,我們進一步說明這個抽象的概念的運用時機:
利用Template Method 可以定義一些共享的算法,差異性部分則由子類別去實作,讓一些有差異性的子類別,擁有共同的算法,如此則程序不但有彈性,且易于維護。
我們應該可以想象,異動交易控制就是一個算法,這個算法會被利用在許多需要控制異動交易的地方,每個異動交易的內(nèi)容是不一樣的,但異動控制的運算卻是一致的。Template Method 這個樣式的重點乃在區(qū)分變與不變的地方,將不變的部分寫成骨架,將變的部分保留起來,對于一個異動交易控制程序來說,異動交易內(nèi)容是變動的部分,而異動控的運算是不變的部分。
【實作范例】
在這個的范例中,我們并沒有使用類別的繼承層級,但卻是運用了另一種方式去實現(xiàn)這個差異性的部分,只要是考慮與現(xiàn)有程序的協(xié)調(diào)問題,至于使用類別層級的方式,在結(jié)論中亦有說明其做法。
首先,在TAdoconnect所在的控制模塊中,增加一個程序,這個程序跟上述的異動控制結(jié)構差不多,只加入了一個TrandProc的事件,在這個事件中,我們將允許執(zhí)行一些差異的運算,因此很簡單的,我們已經(jīng)將Template Method 這個樣式運用在程序設計中了。
function TDmConNection.RunTrans( TransProc:TNotifyEvent):boolean;
begin
try
adoconnection1.BeginTrans;
transProc(self);
adoconnection1.CommitTrans;
result:=true;
except
adoconnection1.RollbackTrans;
result:=false;
end;
end;
接下來,我們來看一下,如何運用這個樣式來解決問提呢,假設你有一個異動程序是這樣:
// 異動程序
procedure TForm1.rename(sender: TObject);
var
n:integer;
begin
adodataset1.First;
for n:=0 to adodataset1.RecordCount-1 do begin
if adodataset1[‘使用者‘]=‘bruce‘ then begin
adodataset1.edit;
adodataset1[‘異動‘]:=checkbox1.checked;
adodataset1.post;
end;
adodataset1.next;
end;
end;
現(xiàn)在,我們要為這個異動程序增加異動控能力,只要增加這樣的一個程序就可以了:
procedure TForm1.Button1Click(Sender: TObject);
begin
if not dmconnection.RunTrans( rename )then
application.MessageBox(‘執(zhí)行失敗‘,‘錯誤‘,0);
end;
把現(xiàn)有的異動程序當成一個參數(shù),傳至RunTrans 這個異動交易控制的運算中,就大功告成。
【結(jié) 論】
這個范例運用Template Method 樣式,避免修改已經(jīng)完成的程序,同時又可以增加系統(tǒng)的功能。當然這個范例,也可以運用類別繼承的方式來實現(xiàn),其寫作的方式,與上述的范例,亦相去無幾,原事件改成宣告一個虛擬的程序,并在子類別中去實作這個程序即可,運用的概念是一樣的,這正是所謂戲法人人會變,巧妙各有不同,融會貫通之后就可以運用自如了。
范例三
運用Factory 工廠模式設計多人使用的權限控制
作者:陳國生
Email: bruce123@edirect168.com
【解決問題】
在一個多人使用的系統(tǒng)中,根據(jù)不同的使用者,必須設計出不同的使用權限,要設計這樣的權限控管機制,很顯然相當復雜而且繁瑣。因此,如何使用一個簡單又容易維護的方式進行系統(tǒng)設計?在軟件設計領域里面,簡單就是美,這是在靈活塑模里面的一個宣言,追求簡單,其實不簡單,同樣的一個問題,有千百種解法,如何使用一個簡單又有彈性的設計,是程序設計者追求的極致,本范例的重點就是要具體實踐這個目標。
【面臨問題】
對已經(jīng)寫好的程序,我們不希望修改,因此如何以擴充功能的方法,將此權限控制的功能加入系統(tǒng)?
【運用技術】
在重整技術中,有一個重要的原則,就是不要去更動已經(jīng)設計好的程序,因此我們將設計一個即插即用的單元,為系統(tǒng)擴充多人使用的控管機制,因此在這個構想下,我們運用Factory 工廠模式的設計樣式來實現(xiàn)這個設計。
什么是Factory 工廠模式?工廠模式可以依實際需要,適時的產(chǎn)生我們所需要的對象,在于無法確定使用何種對象的時候非常好用。在Delphi 中已經(jīng)內(nèi)建許多不同類型的對象,我們可以隨手取用,所以設計一個工廠模式可以說非常的簡單。對象工廠本身有許多變形,如工廠方法( Factory Method ),抽象工廠( Abstract Factory ),雛形( Prototype ) 等等... 皆是。
現(xiàn)在,我們假設每一個使用者登入時,會對應到一個權限對象,然后針對這個對象去設計使用權限即可。換句話說,也就是說讓不同的使用者對應到不同的對象上面,再運用工廠模式,根據(jù)不同的使用者,自動產(chǎn)生不同的對象,如此便可達到權限控制的目的。
當然,控制使用的權限,依控管方式不同,會方法有很多種,下面范例將提供其中一種控管方法。
【范例實作】
第一步:使用一個TDataModule 來存放一些TMainMenu對象。
第二步:TMainMenu 的數(shù)量根據(jù)使用者的多寡而定。
第三步:增加一個TActionList對象,將所有可執(zhí)行程序統(tǒng)一交由TActionList 控管。
第四步:根據(jù)不同的使用權限,將TActionList 里面的TAction 指定至各個不同的TMainMenu 里面的Action 屬性。
以上四個步驟,在Delphi 里面都可以可視化的完成,除了TAction里面的OnExecute必須動手寫入欲執(zhí)行的功能之外。
至此,我們已經(jīng)把準備對應到使用者的權限對象TMainMenu 都設計完成了,接下來只要根據(jù)不同的使用者輸出不同對象即可,這部分要如何設計呢?首先我在TDataNodule 的OnCreate事件中,加入判斷使用者的功能,并輸出對象:
procedure TDataModule1.DataModuleCreate(Sender: TObject);
var
MyMainMenu:TMainMenu;
User:string;
begin
// 根據(jù)不同使用者,取得不同的TMainMenu對象,以下以此類推...
User:=ParamStr(1);
if User=‘001‘ then MyMainMenu:=MainMenu1;
if User=‘002‘ then MyMainMenu:=MainMenu2;
.....
.....
// 接下來輸出對象至主畫面
Application.MainForm.Menu:=MyMainMenu;
end;
以上完成之后,最后的一個步驟是把TDataModule 在項目中自動建構起來,由于設計上是自動輸出對象至MainForm,所以TDataModule 的建構必須在MainForm 之后,這樣就算大功告成了。注意:本范例無處理User 登錄的功能,系使用傳遞參數(shù)的方法來判斷使用者,這部分請自行參考變化。
【結(jié) 論】
工廠模式的對象產(chǎn)生方式,可以動態(tài)產(chǎn)生,也可以靜態(tài)建立,Delphi 中有兩個對象可以很方便的存放這些事先建好的對象,一個是TForm ,一個TDataModule,在本范例中,是利用TDatamodule 存放事先建立好的對象,讓Client 端可以很方便的取得這些對象。
靜態(tài)建立的好處,在于可以利用Delphi 的可視化設計環(huán)境去建構這些對象,因此運用工廠模式及善用Delphi 現(xiàn)有的對象來設計,可以減少許多的程序代碼,增加系統(tǒng)執(zhí)行的穩(wěn)定度,程序代碼寫的愈多,出錯的機會就愈大,相對的系統(tǒng)也不容易維護,這應該是簡單就是美的真諦吧!