| 代碼復(fù)用和界面復(fù)用 面向?qū)ο蟮木幊趟枷霃娬{(diào)代碼的可復(fù)用。而Delphi的精髓實際上就是Object Pascal語言,Object Pascal語言是一個非常強大的面向?qū)ο蟮木幊陶Z言,可以通過對象的繼承實現(xiàn)代碼復(fù)用。同時Delphi作為一個強大的RAD開發(fā)工具,不僅可以實現(xiàn)代碼復(fù)用,還可以實現(xiàn)可視化界面的復(fù)用。 基于復(fù)制粘貼的界面重用 Delphi最早提出的復(fù)用不是面向?qū)ο蟮?#xff0c;而是類似于代碼庫的重用,比如在執(zhí)行窗體右鍵菜單的Add To Repository命令,可以將一些常用的窗體如關(guān)于對話框添加到Delphi的代碼庫中,以后可以在新建窗體時,直接創(chuàng)建一個完全的一樣的對話框。其實這種復(fù)用無法是幫助我們簡化了復(fù)制粘貼的過程而已,會帶來很多后續(xù)維護的問題,過多的使用這種方式編程,會導(dǎo)致大量重復(fù)的代碼,大量重復(fù)的錯誤。而現(xiàn)代的編程思想如XP,則認為不允許復(fù)制粘貼代碼,一旦遇到這種情況,就要進行重構(gòu)。 可視化窗體繼承(Visual Form Inheritance) 可視化窗體繼承,以下我們簡稱其為VFI是Delphi2開始出現(xiàn)的一種軟件復(fù)用技術(shù)。允許我們創(chuàng)建一個基類窗體,并從這個基類窗體派生新的窗體。它在標準的以代碼重用為目的類繼承的基礎(chǔ)上實現(xiàn)了對可視化界面元素的重用。讓我們做個試驗,假設(shè)我們現(xiàn)在編寫一組系統(tǒng)配置管理界面,為了統(tǒng)計界面樣式,規(guī)定所有的配置管理界面都應(yīng)該有一個容器面板,一個確定和一個取消按鈕,由于這樣的界面非常多,為了界面的統(tǒng)一,我們就來創(chuàng)建這樣一個基類界面,首先新建一個項目起名VFI,然后使用 File | New Form菜單命令新建一個界面,起名為TBaseOptionDlg,界面示意圖如下: 然后下面創(chuàng)建一個派生類窗體,用來配置數(shù)據(jù)庫連接的參數(shù),選擇 File | New …菜單,調(diào)出New Items 對話框中,切換到當前的項目VFI下,選中剛才創(chuàng)建的基類BaseOptionDlg,注意在界面的下邊inherit的單選框處于選中的狀態(tài)。點擊確定,就會創(chuàng)建一個新的派生配置管理界面了。 可以看到我們的派生類自動就繼承了父窗體所有的按鈕和面板等界面元素。下面,在主窗體上添加一個數(shù)據(jù)庫連接參數(shù)菜單,添加我們的DB參數(shù)配置界面, procedure TFormMain.N4Click(Sender: TObject); var ? AForm:TDBOptionDlg; begin ? AForm:=TDBOptionDlg.Create(Application); ? try ??? AForm.ShowModal; ? finally ??? AForm.Free; ? end; end; 運行一下后,我突然想起來,一般配置管理界面都會有一個默認值的按鈕,可以用來恢復(fù)默認配置參數(shù)的值,而剛才設(shè)計界面時忽略了這個問題。打開基類窗體,在窗體上放置一個新的默認設(shè)置按鈕,保存后。回過頭來,可以發(fā)現(xiàn)我們的數(shù)據(jù)庫配置界面也神奇增加了一個新的按鈕。想像一下,如果你的工程中需要編寫幾十個配置管理窗體,如果不使用窗體繼承的方式來編寫的話,在程序已經(jīng)進入測試階段時候,客戶突然發(fā)現(xiàn)上面這個問題,要求修改,那么修改的工作量就會非常大,而且很難保證不會因為疏忽而忘記修改某個配置界面。而使用窗體繼承的方式,我們只要修改基類窗體就可以保證修改對所有的派生類都生效。 除了界面繼承之外,VFI也可以實現(xiàn)代碼繼承,在基類窗體的OnCreate事件中顯示一個提示信息對話框: procedure TBaseOptionDlg.FormCreate(Sender: TObject); begin ? ShowMessage('配置參數(shù)界面'); end; 運行程序后,你會發(fā)現(xiàn)雖然我們沒有編寫派生窗體的OnCreate事件處理過程,但是顯示界面時,仍然會彈出消息對話框。 同時,由于窗體的屬性通過VFI被共用,可以有效的地減少占用的系統(tǒng)資源,比如有時我們可能會在界面上放上一個大的圖片進行界面美化,如果這個圖片被放在多個界面中,而這些界面之間沒有繼承關(guān)系的話,圖片就會被多次編譯進資源中,在我們不知不覺中文件大小可能會翻了幾倍。而將圖片放在基類窗體中,無論圖片被多少個子窗體共用,資源都只被編譯一次,因此可以極大的減少生成的可執(zhí)行文件尺寸和加載速度。 VFI窗體屬性及代碼重載 VFI支持繼承,使我們可以重用一些有共性的代碼,但是每個界面又有它特性的一面,這可以通過重載來實現(xiàn)。 比如這回我覺得配置管理窗體上面板的顏色有些單調(diào),想調(diào)整為淡黃色的,但是我又不確定其它人是否會贊同我的審美眼光,所以我不打算修改基類的窗體面板顏色屬性,而只是修改派生的數(shù)據(jù)庫配置界面上面板的顏色為clInfoback,可以看到我們的派生類窗體上面板的顏色變成了淡黃色,但是基類的面板仍然保持不變,也就是說我在子類窗體中對父類窗體的屬性進行了重載。如果修改之后,客戶不滿意我的顏色搭配,而喜歡基類的顏色搭配,有一個簡便的辦法可以恢復(fù)繼承的父類屬性,那就是選中面板,然后執(zhí)行右鍵菜單中的Revert to inherited命令就可以了。見下圖: 除了屬性重載外,Delphi還支持事件重載。接下來,在數(shù)據(jù)庫參數(shù)配置界面上添加一個編輯框,用來指定數(shù)據(jù)庫名,當顯示界面的時候,需要在編輯框中給用戶展現(xiàn)當前配置的數(shù)據(jù)庫名,因此要在窗體的OnCreate事件中進行編輯框內(nèi)容的初始化。雙擊窗體,創(chuàng)建OnCreate事件處理函數(shù),你會發(fā)現(xiàn)新建的OnCreate事件不同于普通的OnCreate事件,窗體設(shè)計器自動在代碼中加了一句Inherited語句,代碼示意: procedure TDBOptionDlg.FormCreate(Sender: TObject); begin ? inherited; ? end; 新加的inherited語句調(diào)用的其實就是基類的OnCreate事件處理過程。前面基類的OnCreate事件中只是簡單的顯示一個消息對話框。如果在派生的TDBOptionDlg中不想顯示那個愚蠢的消息框,只要把inherited注釋掉就可以了。下面是修改后的初始化代碼: procedure TDBOptionDlg.FormCreate(Sender: TObject); begin ? //inherited; ? edtDB.Text:='c:\hubdog.db'; end; VFI的局限 在好的技術(shù)都有它的局限性,不能包制百病,VFI同樣如此。第一個限制就是在派生類的窗體中,我們不能刪除從父類繼承的組件,同時我們也不能象代碼繼承那樣,使用protected等關(guān)鍵字降低某些界面組件的保護級別,使其對于子類不可見,從這一點上來說VFI不能實現(xiàn)界面元素信息的隱藏,這不符合面向?qū)ο蟮姆庋b要求。那么這就產(chǎn)生一個問題,在某些配置界面中,可能我們不想提供基類界面要求的恢復(fù)默認值的操作功能,一個比較丑陋的辦法就是修改默認值按鈕的visible屬性為false。與此相關(guān)的一個問題是,如果基類中定義了一個面板,而在派生類中在面板上放了一個按鈕,如果修改基類界面時將面板刪除的話,則派生類中的按鈕也將被刪除,這類問題處理不好的話,有時會產(chǎn)生很大的混亂。 另外,雖然VFI允許重載屬性和事件,這就會產(chǎn)生另外一個問題,平時很多人習慣了使用事件來實現(xiàn)界面初始化,響應(yīng)按鈕點擊實現(xiàn)參數(shù)配置更新等操作。但是由于VFI默認情況會調(diào)用基類繼承的事件,而如果窗體的繼承層次很多,并且在不同層次上都使用事件處理函數(shù)來實現(xiàn)業(yè)務(wù)邏輯,那么很有可能會出現(xiàn),在不同繼承層次上的事件處理函數(shù)實現(xiàn)了互相矛盾的業(yè)務(wù)處理,比如在繼承樹的中間的某個界面在初始化時,向界面上某個列表框添加了很多字符串,而后面的派生類界面的作者不清楚這個問題,在初始化時先清空列表框的字符串,然后又添加了一些字符串,這就造成了原來信息的丟失。因此使用VFI的時候,建議繼承的層次不要太多,同時盡量使用虛方法來代替事件的使用,對于需要某些強制派生類實現(xiàn)的方法,要用定義純虛方法。 回到我們的參數(shù)配置基類,對于一般參數(shù)配置過程,可以抽象出以下一些共性的必須執(zhí)行的方法: 1、1、?? 顯示界面時,初始化參數(shù)值。 2、2、?? 輸入?yún)?shù)的有效性校驗,如果無效,禁止點擊確定按鈕。 3、3、?? 執(zhí)行參數(shù)的修改,如果成功,則關(guān)閉窗體,如果失敗,則等待用戶重新輸入。 4、4、?? 點擊默認設(shè)置時,恢復(fù)參數(shù)默認設(shè)置。 為了強制實現(xiàn)上面的四個業(yè)務(wù)邏輯,修改后的基類代碼如下: type ? TBaseOptionDlg = class(TForm) … ? public ??? { Public declarations } ??? constructor Create(AOwner:TComponent);override; ??? procedure InitUI;virtual;abstract; ??? function ParamsValid:Boolean;virtual;abstract; ??? function UpdateParams:Boolean;virtual;abstract; ??? procedure DefaultParams;virtual;abstract; ? end; … procedure TBaseOptionDlg.ActionOKUpdate(Sender: TObject); begin ? (Sender as TAction).Enabled:=ParamsValid; end; ? procedure TBaseOptionDlg.ActionOKExecute(Sender: TObject); begin ? if UpdateParams then ? begin ? ??ModalResult:=mrOk; ? end; end; ? procedure TBaseOptionDlg.ActionCancelExecute(Sender: TObject); begin ? ModalResult:=mrCancel; end; … constructor TBaseOptionDlg.Create(AOwner: TComponent); begin ? inherited; ? initUI; end; ? procedure TBaseOptionDlg.ActionDefaultExecute(Sender: TObject); begin ? DefaultParams; end; ? end. 為了實現(xiàn)前面定義的業(yè)務(wù)邏輯,我們在窗體構(gòu)造方法中調(diào)用InitUI過程來初始化界面,而在Action的OnUpdate事件中不停調(diào)用ParamsValid函數(shù)判斷當前輸入的參數(shù)是否有效,如果有效,則允許點擊確定按鈕。點擊確定按鈕后,會調(diào)用UpdateParams方法來更新參數(shù),如果更新成功,則關(guān)閉界面,否則等待用戶重新輸入。最后,在用戶點擊恢復(fù)默認參數(shù)值按鈕時,調(diào)用DefaultParams方法來完成。要注意的是,基類中的initUI,ParamValid,UpdateParams和DefaultParams過程都被定義為純虛的抽象方法。這是因為對于基類來說,由于沒有具體的參數(shù)輸入界面元素,實現(xiàn)這些方法是沒有意義的,只有到了具體的參數(shù)配置界面,才需要實現(xiàn)這些方法,同時定義為Abstract抽象方法可以強迫派生類必須實現(xiàn)這幾個方法來完成業(yè)務(wù)邏輯,否則編譯后無法正確運行。 下面是TDBOptionDlg實現(xiàn)的示意性代碼: procedure TDBOptionDlg.DefaultParams; begin ? edtDB.Text:='c:\hubdog.db'; end; ? procedure TDBOptionDlg.InitUI; begin ? edtDB.Text:='c:\hubdog.db'; end; ? function TDBOptionDlg.ParamsValid: Boolean; begin ? Result:=trim(edtDB.Text)<>''; end; ? function TDBOptionDlg.UpdateParams: Boolean; begin ? Result:=True; end; ? end. 另外,窗體繼承不支持窗口嵌套,也就是不支持窗口的組合復(fù)用,因此為了擴充窗口的功能,無形中鼓勵人們使用繼承機制,而不是組合機制。這加深了窗口之間的偶合度,不利于靈活性和擴展性。 窗體繼承還有一個很大的問題就是它只支持窗口級別的組件復(fù)用,但是更多的時候,我們想要的只是粒度更小的界面中某一部分顯示區(qū)域或者組件級別的復(fù)用。Borland也考慮到了這個問題,因此在Delphi4中給出了Component Template的解決方案。 組件模版模板 組件模版模板技術(shù)相當簡單,比如下圖所示意的雙列表框組合界面是很常用的一類界面,可以用來從一個列表框向另一個列表框移動對象。 那么我們就可以選擇列表框和按鈕,然后執(zhí)行 Component | Create Component Template...命令來創(chuàng)建組件模版模板,示意圖如下: 設(shè)定組件模版模板名稱為TDualListBox,并將其作為一個組件放到Templates組件面板上,組件面板除了能保存各個組件的屬性外,還可以保存相關(guān)組件的事件代碼。還有一點限制是它不能將窗體設(shè)定為組件模版模板。 生成好的組件模版模板可以象其它組件一樣從組件面板上拖放到窗體,并生成模版副本。組件模版模板同VFI本質(zhì)上的不同在于它不能實現(xiàn)繼承,當把TDualListBox作為一個組件放到窗體上之后,它只相當于原來所有的組件的一個拷貝,我們可以隨意地將一個按鈕刪除,而VFI的派生類則不允許我們刪除從父類繼承的組件。因此,組件模版模板不能算是一個面向?qū)ο蟮慕缑鎻?fù)用解決方案,即便我們隨后修改了組件模版模板的設(shè)計,這些設(shè)計的變化并不會影響到先前的被實例化了的組件。因此可以認為,組件模版模板只是一個簡化我們操作的復(fù)制粘貼工具。 另外,組件模版模板還有一個很致命的問題就是,它很難被共享,因為組件模版模板信息是被統(tǒng)一保存到Delphi.dct文件中的,而不是保存在pas文件中的,因此要想在不同機器間共享模版,必須將Delphi.dct拷貝到其它機器上,或者將Delphi.dct進行共享,而Delphi.dct又是一個單獨的文件,你無法從中抽取單獨的一些模版模板出來分發(fā)。所以,Component Template不太適用于多人開發(fā)的項目中。 針對VFI和Component Template等技術(shù)暴露出來的問題,Borland進一步的提出了TFrame的解決方案。 基于TFrame的復(fù)用 TFrame比較像一個綜合了Component Template和VFI優(yōu)點的產(chǎn)物,本身的實現(xiàn)機制同VFI非常類似,都是面向?qū)ο蟮?#xff0c;對基類的可視化變更會立刻反映到派生類,支持代碼共享和界面共享,很容易分發(fā)。同時,它又像Component Template那樣支持小粒度的組件復(fù)用,支持子窗口嵌套,是一個比VFI更為輕量級的解決方案。 對于那些非常復(fù)雜有很多輸入選項的獨立的界面編程,TFrame是非常適合的,因為利用TFrame我們可以將一個復(fù)雜的界面分解為多個簡單的模塊的編程。假設(shè)這回客戶要求我們編寫一個客戶資源信息錄入界面。客戶資源信息包括很多內(nèi)容,比如Email,電話,地址幾十項信息。 分析一下客戶對象這個實體的屬性,一個客戶可以對應(yīng)多個Email地址 ,多個電話號碼,對于這種一對多的關(guān)系,可以采用一個輸入框,一個列表框和添加,刪除按鈕來完成信息的編輯修改的,同時考慮到電話和Email地址同客戶的關(guān)系都為一對多,那么就需要編輯組件和大同小異的功能實現(xiàn)。對于這種情況,我們就可以使用TFrame來復(fù)用這樣的一對多信息輸入界面。 首先,新建項目,在主界面上添加一個面板,然后在窗體上放上確定和取消按鈕。然后,使用菜單命令 File | New Frame新建一個TFrame。在新建的Frame上添加列表框等輸入組件,完成的界面示意圖如下: 為Frame添加信息編輯的代碼 type ? TFrameList = class(TFrame) … ? protected ??? function CanAdd:Boolean;virtual; … ? end; … procedure TFrameList.ActionAddUpdate(Sender: TObject); begin ? (Sender as TAction).Enabled:=CanAdd; end; ? function TFrameList.CanAdd: Boolean; begin ? Result:=(trim(edtInput.Text)<>'') and (ListBox.Items.IndexOf(trim(edtInput.text))>-1); end; ? ? procedure TFrameList.ActionAddExecute(Sender: TObject); begin ? listbox.Items.Add(trim(edtInput.text)); end; ? procedure TFrameList.ActionDelUpdate(Sender: TObject); begin ? (Sender as TAction).Enabled:=ListBox.ItemIndex>-1; end; ? procedure TFrameList.ActionDelExecute(Sender: TObject); begin ? ListBox.Items.Delete(ListBox.ItemIndex); end; FrameList使用Action對添加刪除動作會進行有效性判斷,添加時調(diào)用函數(shù)CanAdd判斷是否可以添加,FrameList基類的CanAdd函數(shù)只是簡單的判斷當前輸入框中文本是否為空,以及當前輸入框中文本是否已經(jīng)被添加進了列表框,如果不滿足,禁止添加按鈕。注意為了派生類擴展的需要,這里CanAdd聲明為虛方法,后面我們的Email和電話輸入Frame要想對Email和電話的有效性進行校驗的話,可以重載這個函數(shù)。刪除前只是簡單的判斷ListBox中是否有選中的要被刪除的信息。 接下來就是從我們的基類派生出Email和電話的編輯框架,其中電話的編輯框架只是修改的按鈕和標簽的Caption,顯示添加電話,刪除電話,以及電話列表等信息,同時重載了CanAdd函數(shù),提供了對電話號碼的簡單判斷。而Email除了修改顯示信息和重載CanAdd函數(shù)外,還為列表框增加了雙擊列表框,激活向Email地址發(fā)送郵件的功能。 //判斷是否可以添加電話號碼 function TFrameTele.CanAdd: Boolean; var ? I, code:Integer; begin ? if inherited CanAdd then ? begin ??? Val(trim(edtInput.Text), I, code); ??? Result:=code=0; ? end; end; ? //判斷是否可以添加Email function TFrameEmail.CanAdd: Boolean; begin ? if inherited CanAdd then ? begin ??? result:=Pos('@', trim(edtInput.Text))>0; ? end; end; //激活Email客戶端,收件人為當前Email賬戶 procedure TFrameEmail.ListBoxDblClick(Sender: TObject); begin ? inherited; ? if ListBox.ItemIndex>-1 then ??? ShellExecute(Handle,'open', PChar('mailto:'+trim(edtInput.text)),nil,nil,SW_NORMAL); end; ? 注意由于本例子只是演示Frame的用法,所以我只是簡單的進行輸入有效性判斷,真正完備的判斷應(yīng)該是基于正則表達式的,雖然VCL庫中沒有提供正則表達式的支持,但是有一些免費的第三方庫,比如TRegExpr可以使用,這里就不詳述了。 接下來選中面板,然后點擊組件面板Standard頁面上的Frames圖標,調(diào)出Frames列表框, 在面板上添加FrameEmail和FrameTele, 接下來是編寫界面初始化代碼來加載,這時你會發(fā)現(xiàn)TFrame不同于TForm,它沒有提供OnCreate和OnDestroy事件(不知道是什么原因,我猜測Borland的R&D Team一定也研究這個問題,不知是出于什么考慮從Delphi5到Delphi7一直沒有實現(xiàn)這一顯而易見的需求),所以要想在TFrame創(chuàng)建時對其進行初始化,只能是在TFrame的OnCreate事件中進行初始化。 constructor TFrameEmail.Create(AOwner: TComponent); begin ? inherited; ? ListBox.Items.Add('hubdog@263.net'); ? ListBox.Items.Add('hubcat@263.net'); end; constructor TFrameTele.Create(AOwner: TComponent); begin ? inherited; ? ListBox.Items.Add('861088888888'); ? ListBox.Items.Add('861066666666'); end; 可以看到,使用TFrame后,原來需要集中在主界面完成的代碼,現(xiàn)在全都分散到各個單元來實現(xiàn),同時TFrame可以嵌套在主界面中實現(xiàn)可視化修改,甚至TFrame中也可以繼續(xù)嵌套TFrame,將TFrame想像成建筑中的磚頭,工人可以通過磚頭的堆砌和組合建立起摩天大樓,同樣的我們通過TFrame的組件組合的復(fù)用模式,也可以實現(xiàn)操作復(fù)雜的交互界面來。 TFrame的局限性 雖然TFrame有著很多的好處,但是也一樣有它的缺點,比如它和VFI一樣,無法實現(xiàn)信息隱藏,因為界面上所有的組件默認都是published屬性,并且無法像代碼那樣通過protected等保護級別關(guān)鍵字進行隱藏,它暴露了內(nèi)部的太多的實現(xiàn)細節(jié),不滿足面向?qū)ο笏枷胫姓涌诘姆庋b原則。要想實現(xiàn)真正的信息隱藏,必須通過純代碼方式編寫的組件來實現(xiàn),但是編寫組件雖然能滿足封裝的原則,但是無法像TFrame那樣無須編譯注冊,就可以在設(shè)計時可以通過窗體設(shè)計器隨時修改可視化設(shè)計,做到所見即所得,正所謂魚和熊掌不可得兼。在這方面Sergey Orlik給出了一個比較好的解決方案,他編寫的Custom Containers Pack可以做到可視化設(shè)計復(fù)合組件,并通過編譯來實現(xiàn)封裝,這里限于篇幅的原因,我就不加以介紹了。 ? ? 另外,對于某些屬性復(fù)合組件,在TFrame使用時,經(jīng)過修改后可能會產(chǎn)生異常的效果。舉例說明,新建一個Frame1,在Frame上放上一個TTreeView,在樹視圖中建立一個標題為1的節(jié)點。然后從TFrame1派生一個新的Frame,然后在派生Frame中向TTreeView再添加一個標題為2的節(jié)點。回過頭去,在基類TFrame1中,向樹視圖添加新的標題為3的節(jié)點,再打開派生的TFrame2,你會發(fā)現(xiàn)基類中添加的節(jié)點3并沒有被繼承到TFrame2中,這是因為樹視圖對樹節(jié)點信息進行編碼后是保存在Data屬性中,見下表: ? object TreeView1: TTreeView ??? Left = 40 ??? Top = 16 ??? Width = 265 ??? Height = 121 ??? Indent = 19 ??? TabOrder = 0 ??? Items.Data = { ????? 010000001A0000000000000000000000FFFFFFFFFFFFFFFF0000000001000000 ????? 01311A0000000000000000000000FFFFFFFFFFFFFFFF00000000010000000132 ????? 1A0000000000000000000000FFFFFFFFFFFFFFFF00000000000000000133} ? end 而窗體設(shè)計器還無法智能到理解這種自定義流屬性的意義,所以設(shè)計器不對這類屬性的修改進行繼承,導(dǎo)致了上面的古怪現(xiàn)象。不僅TTreeView會出現(xiàn)這種問題,包括TListView等包含復(fù)合屬性的控件都有可能出現(xiàn)這類問題。所以,在設(shè)計TFrame時,一定要謀定而后動,盡量在實現(xiàn)TFrame基類時把事情想周全之后再動手,因為后面如果對基類TFrame進行大的改動,會牽一發(fā)而動全身,可能會造成對所有基類的修改,這時就不是節(jié)省工作量,而是增加工作量了。 總結(jié) VFI和TFrame是可以用來創(chuàng)建復(fù)合組件容器,進行所見即所得的可視化編輯,并支持可視化繼承和重載。使用TFrame,我們可以構(gòu)造內(nèi)部緊密偶合,而同外部松散偶合可重用的界面構(gòu)造的Block。可以使我們的界面設(shè)計更靈活,更容易擴展和維護。 |