設計可維護的應用程式

作者:Taco Oosterkamp
編譯:蔡煥麟
校稿:陳國生
日期:Oct-27-2001

譯者說明

這份文件我已經取得作者的同意,將內容譯成中文並且公佈在網站上。

關於中文譯名的拿捏,我盡量使用常見的譯名,只有少部分的專有名詞及術語不譯成中文,例如:form, datamodule, webmodule 等,我想讀者應該都對這些名詞很熟悉了,就讓它們保持原味好了。

有時候為了文句通順或者充分表達作者的意思,在翻譯時我會(多事)自行加入一些補充性的字句,或者(偷懶)將有些字句略過不譯,因此跟原文可能會有一點點的落差,但總的來說應不致於失去作者的原意。

如欲觀看原文的話,可以前往作者的網站,網址是: http://www.topdelphi.nl/

1. 什麼是 「可維護性」?

1.1. 簡介

在這份文件裡面,我提出了一些關於如何建立可維護的應用程式的觀念,我第一次針對這個主題發表談話是在 1997 年於德國 Raunheim 舉行的 Borland 研討會上。

身為一名 Delphi/JBuilder 講師,我所關注的焦點顯然都集中在 Borland 的產品上面,而且我也相當喜歡這些產品。在本文中的許多想法都已經用 Delphi 實際驗證過,在說明一些觀念的時候也會以 Delphi 程式碼來輔助說明,但這些觀念也同樣適用於 JBuilder 和 C++Builder 甚至其他的開發工具。

軟體程式的生命週期是難以預測的,就拿 Cobol 來說好了,回到 Cobol 發展初期的年代,誰能料想到時至今日(1997年)我們仍然在使用 Cobol 程式?又有誰會想到因為它們只用兩個位數來代表年份而可能造成千禧年危機?
這是軟體生命週期比較長的例子,然而另一方面,目前以 Delphi 開發的應用程式當中,許多都會在三五年甚至兩年內就從市場上消失,當中有些程式甚至連使用者的需求都還沒有完成就壽終正寢了。

「可維護性」與應用程式是否易於修改可說是息息相關。大型應用程式的修改需求會出現在開發時期,而小型應用程式的修改需求則常在交付給客戶之後才發生。以現代的商業處理模式來看,改變是必然而且頻繁的,早期應用程式的維護修改工作只是偶爾為之,現在則是持續不斷地改變,於是「維護」成了應用程式生命週期中的常態,對大型商業系統來說更是如此。

我們可以確定一件事:應用程式遲早都會要修改的。所以,就讓我們一塊兒來學習設計易於維護的應用程式吧!

Taco J. Oosterkamp
delphi/jbuilder 認證講師

2. 為什麼要談論和學習「可維護性」?

我得先聲明,「可維護性」這個主題涵蓋的範圍很大,這份文件所提到只能算是皮毛而已。而這個議題之所以重要,是因為了解它可以讓你在設計程式時做出正確的決定。

在建構軟體的過程中,如果沒有事先做好規劃設計的工作,程式寫到後面就會亂得跟一盤義大利麵一樣,相信每位開發人員都有過類似的親身體驗。可是,問題的根源到底在哪兒?如果你一開始就知道問題出在哪裡,你就比較可能在初期就做出正確的決策,程式寫到後面會愈改愈好。

軟體是否能準時上市和你投注在「可維護性」上面的心力多寡成正比,它也能夠提高用戶以及開發人員的滿意度。

要建立一個易於維護的應用程式,你必須確保軟體開發過程中的每一個環節都經過健全的設計,以下我將會以這個角度為出發點來探討如何設計出易於維護的程式。

2.1. Delphi 是 RAD 工具嗎?

RAD(Rapid Application Developement,快速應用程式開發)對許多人來說,其涵義是倉促的、草率的、而且錯誤百出的決策過程,它們欠缺完善的規劃和深思熟慮的設計,品保也做得不好。在這種情況下,呃.....Delphi 可以是個很棒的 RAD 工具。

當然啦,RAD 它可以是,也應該是完全另一種樣子。
Delphi 是一個很棒的全方位開發工具,你可以用它來開發各式各樣的應用程式,到了 Delphi 3 加入了三層式架構的能力,更使它成為開發資料庫應用程式的神兵利器。

也正因為 Delphi 全方位的特性,它具備了開發各種應用程式的一般性的功能,卻也讓開發人員在撰寫程式時有了過多的自由,任意揮灑的結果通常就是設計不良的程式,這使得 Delphi 背負了一些莫須有的罪名。

在過去幾年當中,我曾見過許多糟糕的 Delphi 應用程式,以及因為上述原因而屢遭挫敗的專案經理與程式設計師,其實這一切和 Delphi 本身並無多大關係,真正的關鍵在於你必須瞭解如何正確的使用 Delphi,以及思考如何讓程式更易於維護,我會在後文中做更進一步的介紹。

3. 哪些因素改善了可維護性?

3.1. 切割應用程式

欲建立可維護的應用程式,首要工作就是將應用程式切割成各個小塊,並且透過一組方法(程序及函式)呼叫來完成任務,這也就是結構化程式設計的觀點,這個觀點推展到了物件導向程式設計,更進一步地將方法資料封裝在一起。

如果你能夠把一個大程式切割成幾個較小的獨立模組,每一位開發人員就可以將心力完全投注在自己負責的模組,而不至於被盤根錯節的複雜流程搞得手足無措。

在後面所提到的幾個術語,像是內聚力耦合,大部分都只會描述它們與切割應用程式之間的關係。

我們會用許多種不同的名詞來稱呼這些被切割出來的部分,例如:零件,元件,模組,單元,類別,物件...等。而這些名稱在 Delphi 裡面各自都有完全不同的意義或者某種程度的關係。

在很多情況下我會使用「單元」(units)一詞來代表一般性的統稱,但並不是說所有的單元都是一個獨立的 .pas 檔案(程式單元),有時候我會用「單元」來代表許多的 .pas 程式檔案。

Delphi 提供了許多種切割程式的方式,你可以把程式碼分成幾個單元(.pas 檔案)、類別(包含 forms,datamodules,webmodules)、專案以及目錄。以上列舉的幾種方法當中,我們比較常用的是類別,把每個類別放在一個獨立的程式單元中,維護工作會變得更為簡單容易。在 JBuilder 中這是唯一的方法,因為所有 Java 的程式都必須以類別的方式呈現。

為什麼要使用單元來切割,而不將它們寫成一坨又臭又長的程式呢?理由是開發人員可以:

一個程式單元通常會使用數個其他的單元,這使得在你的應用程式中建立數個抽象層成為可能,特別是比較大型的應用程式,你需要用不同程度細節的角度去審視它,瞭解它。

所以,你應該要能夠以圖形來表示應用程式中各單元的關係,這樣你才能確實瞭解它們各自扮演的角色,以及它們之間是如何互動的。

接著就讓我們來看看一些關於應用程式切割的理論,了解一下它們的作用是什麼,以及它們為什麼這麼重要。將這些理論實際應用在程式設計上面將可以協助你做出正確的決定。

3.2. 內聚力(Cohesion)

顧名思義,內聚力指的就是事物凝聚的狀態或程度。程式的內聚力則是愈強愈好。

一個高內聚力的程式單元,其中的組成分子(包括方法,資料以及類別)之間的關係是嚴密而且不可分割的,它們的關係愈是緊密,內聚力就愈高。

高內聚力的程式有個好處,就是一旦程式需要修改的時候,只需要更改在同一個單元裡的程式碼。我們不希望程式會牽一髮而動全身,因為這樣就完全破壞了切割應用程式的美意。

3.2.1. 使用 DataModules

在 Delphi 程式設計中有一個跟內聚力至關重要但是卻經常被忽略的東西,它就是 DataModule。我曾見過許多人把 TTable 和 TQuery 元件直接放在 form 上面使用,是很方便,但是程式卻變得不好維護了。

怎麼說呢?首先,當你有許多個 form 需要存取相同的資料表時,你把這些存取到相同資料表的 dataset 元件放在各個 form 上面,這樣一來你就會在各個 form 上面為 dataset 元件撰寫相同的事件處理程序。其次,在 form 所屬的單元裡頭會夾雜著一堆跟使用者介面無關的程式碼,使整個程式碼看起來更加雜亂。假使現在程式要改成存取別的資料表,或者使用不同的商業規則,甚至將鍵盤輸入資料的部分改用巨集命令來取代,很多工作就得重頭再來一遍,因為你的程式碼根本無法重複使用。

Form 單元本來就應該只負責一種工作,現在卻放入了使用者介面、資料存取以及各種事件處理函式的程式碼,程式的內聚力便因此而降低。

3.2.2 使用更多的 DataModules

還有一種常見的情形同樣也違背了內聚力的設計原則,就是整個專案裡面只有一個 datamodule,整個應用程式中需要用到的資料存取元件(TTable, TTQuery...等)都放在這個 datamodule 裡面。

然而一個設計良好的的應用程式,通常都會使用多個 datamodule 來分別處理不同的工作,例如,一個 datamodule 負責處理客戶資料,一個負責處理訂單,一個處理發票....等等。你甚至可以將這些資料處理的作業分成統計分析和交易處理兩種類型,然後將它們封裝成個別的 datamodule;你也可能會有一個 datamodule 是用來檢視客戶資料,而另一個則專門負責編輯資料。

這麼做還有個額外的好處,就是你可以在需要用到 datamodule 的時候才建立它們,這樣不但能節省記憶體,還能減少應用程式啟動的時間。當然啦,對於第一次使用這種方式撰寫程式的人來說可能稍嫌麻煩了些,畢竟它不是那種「將 TTable 放到 form 上面然後把 Active 屬性設成 True」這麼容易的工作,但是話說回來,讓開發人員的生活更輕鬆並不是我們學習軟體工程的主要目的啊,您說是吧?

以上所提到的有關使用 datamodule 的原則,同樣也適用於其他類型的模組,總之,應用程式的內聚力愈高,除錯和維護的工作就愈輕鬆。

3.3. 耦合度(Coupling)

Coupling 的字面意義為:

  1. 連接車箱;一種連接各個機械的裝置。
  2. 兩個相互連接的系統,改變其中一個就會使另一個系統運作失常。

用在程式設計的領域時,表示程式單元之間牽連相依的程度,單元之間的耦合度欲寬鬆愈好。

程式單元之間需要相互溝通,就像大型的跨國企業一樣,各分公司必須使用某種比較正式的方式來傳送資訊,如果你有事情需要美國那邊的同事幫忙處理,不能夠只發個 email 就算了,你得擬一份正式的信函,經過上面層層主管的認可之後,才能飄洋過海到達另一個部門。

電腦系統也是一樣,你會希望資訊只能夠經由正常的管道在單元之間傳遞,而不要有任何走後門的事情發生。當你的程式裡使用了不正當的方法分享資訊,單元之間的耦合度就會增加,因而導致下列情形:

你真正需要的是透過介面來制定程式的溝通方式。

3.3.1. 引用其他 form 上面的元件

許多時候,我們會直接使用其他 form 上面的元件,例如:

procedure TForm1.DoIt;
begin
  Form2.Edit1.Text := 'test';
end;

上面的程式碼雖然可以正確執行(uses 敘述裡面必須加入 form2 的單元),但卻是不易維護的。

首先,我無法從元件的名稱看出來它們的作用是什麼,程式碼是給人看的,自然該寫得讓人能看得懂它在做什麼,'Form2' 對我來說不具任何意義。比較好的名稱可能是 EditCustomerForm 或 MainForm 或 PrintInvoicesForm。

其次,當我改變了 Form2 上面的 Edit1 元件麻煩就來了,假設我把 Edit1 元件改成了 combobox,我就得修改專案裡面所有用到 Edit1 的程式碼,這對 Form2 本身來說不至於影響太大,但是如果這個修改動作得涉及到遠端伺服器上的所有不知名的程式單元,此時麻煩可就大了,這問題我可是親身經歷過的。所以,如果你在程式中直接參考其他單元的元件,你就無法確定有哪些地方沒有改到。

如果單元之間的耦合太過緊密(如前面的例子),程式碼維護起來就更花時間。

而解決方法就是:「資訊隱藏」與「間接存取」。

3.4. 資訊隱藏

「資訊隱藏」的主要觀點在於有些資料只允許特定的對象存取,因此將這些資料隱藏起來不讓外界任意存取,要存取這些資料一定得透過特定的介面。

這麼做有個很大的好處,就是可以避免其他的程式破壞我的資料,而且如果我要修改一個不屬於公開介面的方法時,我可以確定其他的程式都不用改,唯一要修改的地方就只有自己的程式單元而已。

資訊隱藏這項極為重要的設計原則可以很輕易的在 Delphi 程式中實現,接下來你就可以看到實際的做法。

3.4.1. 單元

Delphi 的每個程式單元主要分成 interface 和 implementation 兩個部分,看看下面這個很單純的範例:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;

type
  TForm1 = class(TForm)
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

procedure DoIt;
begin
  ShowMessage(' DoIt ');
end;

end.

單元的第一個部分是從關鍵字 interface implementation 之前的區塊,在這個區塊當中所宣告的型態,變數,函式等都可以被其他的單元使用,可以說是單元的介面部分;從關鍵字 implementation 以後就是第二個部分,在這裡宣告的任何東西都無法被其他單元使用,譬如範例中的 DoIt 函式就無法被其他單元呼叫,這就是一個「資訊隱藏」的例子。

宣告在介面部分的型態 TForm1 和變數 Form1 在整個應用程式的任何地方都可以存取,因此 Form1 就是一個全域變數。

3.4.2. 類別

在類別的宣告中,你可以用幾種不同的指示字(directives)來指定存取範圍,並藉以控制資訊隱藏的程度,看看這個類別:

type
  TMyClass = class(TObject)
  private
    FPrivateField: integer;
    FMaster: string;
  protected
    procedure DoIt;
  public
    function GetYou: string;
  published
    property Master: string read FMaster write FMaster;
end;

宣告在 private 裡面中的成員(包括變數和函式)只能被類別的成員函式使用,外界無法存取;而宣告在 protected 裡面的成員則只能有類別本身及其衍生類別才能存取,外界亦無法存取;public 和 published 成員則可以讓類別和外界存取。因此在這個例子裡面,FPrivateField 不能被外界存取,DoIt 函式只有類別及其衍生類別才能使用。

我們可以了解到,類別藉著不同存取範圍提供了不同程度的資訊隱藏。

美中不足的是,那些放在 form 上面的元件都會被宣告在 private 之前,在這裡宣告的成員 Delphi 視其為 published,如果想要將他們改為宣告成 private,使外界無法直接存取這些元件,唔....這是不可能的--至少到目前為止我還未聽說任何方法可以這樣做而程式還能正確執行。

譯註

當你把 form 上面的元件宣告移到 private 區域時,可以通過編譯,但是執行時會出現錯誤:

  Exception EClassNotFound in module vcl50.bpl at XXXXXXXX.
  Class XXX not found.

此問題有個辦法可以解決,就是在 initialization 區段手動去註冊這個類別,現在假設我把 Button1 的宣告移到了 private 區域,只要加入以下程式碼,程式就可以正確執行了:

initialization
  RegisterClass(TButton);

然後,費了一番功夫之後,卻只是為了將元件隱藏在 private 區域,這麼做並不實際,這裡提出的方法只是供讀者參考而已。

3.5. 介面

介面定義了各單元之間所需的溝通方式,你可以把介面想成是一組用來傳遞特定資料的函式。

就技術上而言,介面是由一個單元的公開方法和變數所組成,它們可能是標準的程序、函式、屬性、事件、及變數(物件也算)....等等。當單元 A 的一個方法(method)可以被單元 B 呼叫的時候,該方法即屬於單元 A 的介面,而單元 B 就可以經由呼叫這個方法和單元 A 溝通。

通常一個單元會提供數個方法供外界呼叫,若能將這些方法結合並形成一個正式的介面,那麼程式將更易於維護,而且像這樣的介面應該都要撰寫適當的說明文件。

基本上,介面應該是一個單元裡唯一可以被外界「看得見」的部分,單元的其他部分都應該被隱藏在內部(資訊隱藏)。

3.6. 間接存取(Indirection)

間接存取,顧名思義就是非直接的存取。當你使用間接的方式存取資料,你並未真正碰觸到你所想要存取的資料,而是透過一個中間人來完成(譯註:就像你到銀行提款必須經過櫃檯小姐,而不是自己大剌剌的去金庫裡拿錢),這個中間人則提供了一道控管的機制,讓你可以針對存取動作撰寫額外處理的程式碼,這便是我們要使用間接存取的主要原因。

舉例來說,假設你有一個單元 unitC,裡面有個名叫 variableC 的變數,該變數是可以被外界存取的(宣告在 interface 區段),於是其他單元就可以很方便的存取 unitC 裡面的 variableC。

但問題是當 variableC 原本的運算或處理規則改變了,你就得巡視一遍所有的程式單元,並且修改跟 variableC 有關的程式碼。現在假設這個 variableC 儲存的是一個員工的薪資,而且程式已經上線使用了,此時上頭卻要求你必須在員工薪水有異動時加一道檢查手續,確保員工的薪資不會比老闆的薪資還高。這時候你怎麼辦?

如果你之前就採用間接的方式來存取這個變數,這個改變對你來說就不成問題了。你會將變數隱藏在外界無法存取的私有區域,並且定義兩個方法來分別負責讀取和寫入變數的動作。「這種做法手續比較多耶!」,沒錯,可是一旦之前的異動需求發生了,你就只要修改那個負責寫入動作的方法就行了。

在 Delphi 裡面有許多種實現間接存取的方式,我會在加下來的幾個小節裡依序介紹它們。

3.6.1. 方法

程序與函數是實現間接存取時最常用的方式。假設一個 form 上面有一個代表客戶名稱的 edit 元件,而你要修改其 Text 屬性來改變客戶名稱,一般的寫法是:

CustomerForm.edtCustomerName.Text := 'test';

更好的做法是在 CustomerForm 裡面加入一個方法,透過呼叫這個方法來改變 edit 元件的屬性,像這樣:

interface

type
  TCustomerForm = class(TForm)
  private
    { Private declarations }
  public
    { Public declarations }
    procedure SetCustomerName(Value: string);
  end;

implementation

procedure TCustomerForm.SetCustomerName(Value: string);
begin
  edtCustomerName.Text := Value;
end;

當你要改變客戶名稱時就這麼寫:

CustomerForm.SetCustomerName('test');

我知道這種寫法得花較多的時間,而且程式看起來比較複雜,可是當你的程式需要修改時,例如將 edit 元件改成 combobox,或者其他任何狀況,你所要做的仍然只是修改 SetCustomerName 這個方法而已。如果你不使用這種間接的方式,你就必須修改所有使用到 edtCustomerName 的單元。

3.6.2. 屬性

屬性也是間接存取的典型用法,它能讓你以間接的方式存取隱藏於類別裡的資料。以下是一個典型的屬性宣告的例子:

{ 取自 forms.pas }

TControlScrollBar = class(TPersistent)
private
  FMargin: Word;
published
  property Margin: Word read FMargin write FMargin default 0;
end;

屬性 Margin 代表的是一個邊界值,而對屬性 Margin 的讀寫動作都會直接作用於私有成員 FMargin 身上,看似多此一舉,但事實並非如此。如果日後我們要改變屬性 Margin 的行為(例如,加入程式碼以檢查邊界的值是否正確),我們就可以加入一個專門用來設定屬性 Margin 的函式:

{ 取自我的想像 }

TControlScrollBar = class(TPersistent)
private
  FMargin: Word;
protected
  procedure SetMargin(Value: Word);
published
  property Margin: Word read FMargin write SetMargin default 0;
end;

implementation

procedure TControlScrollBar.SetMargin(Value: Word);
begin
  if Value <> FMargin then 
  begin
    // 這裡可以加入數值檢查的程式碼,如果邊界值不正確就舉發異常.
    FMargin := Value;
  end;
end;

這種彈性的做法對 Delphi 來說是相當重要的,因為它讓我們在設計元件時能夠對屬性的存取動作有更多控制的機會。此外,我們之所以能夠在設計時期透過物件檢視器(object inspector)改變元件的屬性,也都是透過宣告於 published 區域的這些屬性。

重要:屬性不只用在元件設計,其實在 form 裡面也可以加入屬性(這點比較少人注意到),只是你 form 裡面自行加入的屬性不會在物件檢視器中顯示出來,就這個差別而已,Delphi 的 Code Insight 功能還是能夠顯示你自己宣告的屬性,當然你也可以在程式中使用它們。

3.6.3. 事件

事件也可以使用間接存取,請參考本文所附的範例程式。Form2 有一個事件會去呼叫 Form1 的方法,在編譯時期,Form2 完全不知道 Form1 的存在,直到程式執行時,Form1 才動態地將 Form2 的事件掛(hook)進來,也就是說不只是 Form1,其他的 form 也可以用同樣的方式重複使用 Form2。

至於是否要透過哪種方式或途徑來實現事件的間接存取,就就視個人需求而定了。

3.6.4. COM 介面

Delphi 3 為了支援 COM 類別設計而增加了一個新的關鍵字:interface。然而,並不是只有在設計 COM 元件時才能使用它,你在任何的 Delphi 程式中都可以使用 interface 來達成間接存取及抽象的目的。

然而,我並不打算在這份文件中闡述介面程式設計的相關理論。

譯註

COM(Component Object Model)是微軟提出的一種元件模型,是一種以介面為基礎的程式設計方式。從 Delphi 3 開始我們就可以使用 interface 來定義元件的介面,並以 class 來實作該介面。定義介面的方式跟類別有些相似,看看以下這個極為簡單的例子:

IMyInterface = interface
  procedure SayHello;
end;

很明顯的,這裡的 interface 和前文所提到程式單元的 interface 區塊是兩種不同的東西。限於篇幅,這裡也只能點到為止,如欲進一步了解 COM 以及介面程式設計 的方式,請參考相關書籍。

3.7. 使用介面

如同我在前面說明「耦合度」時所提到的,你應該定義自己的介面,而不要直接使用 Form2.Edit1.Text 的方式來存取其他單元裡的資料。

在定義介面時應盡量保持介面的一致性,介面要輕巧而且容易重複使用。

3.8. 簡單與標準化讓事情更容易

欲建立易於維護的應用程式,有個很好的方法,就是盡量保持簡單。如果你能夠將程式的內涵與結構解釋得連我的女兒都聽得懂,那我相信你也絕對可以維護這個程式。

3.9. 使用命名慣例及有意義的名稱

如果你希望程式是可維護的,在為變數、元件、以及檔案命名時就不可以草率。我自己也有使用一套命名慣例,命名的方法是先為變數、元件、或檔案取個有意義的名稱,然後在前面加上其型態的名稱,兩者之間加上一個底線。

譯註

作者在 3.9.1 及 3.9.2 節分別示範性的列出了幾個檔案及元件的命名方式,例如:unitform_Main, form_Customer, datamodule_Customer, button_Ok.....等。

這種命名方式我個人覺得稍嫌冗長,倒不如使用 Delphi 5 Developer's Guide 書中建議的命名方式,因此我決定不將 3.9.1 及 3.9.2 節的內容譯出來,建議您可以參考下列文件:

3.10. 文件

在軟體設計的過程中,撰寫說明文件的工作總是到最後才做,而且經常是在進度落後的情況下,以平日隨手寫下的筆記拼湊出來的。

簡短的一句話,卻隱含著數十年來軟體開發過程中遭遇的挫敗與慘狀。正確的做法應該是在軟體開發的每一個階段當中就要一邊撰寫說明文件,而不是等到軟體快要完成的時候才開始製作文件。

這觀念對程式的維護來說很重要,可是卻常被人們忽略。當一個程式設計師要修改一份既有的程式碼時(該程式也有可能是他自己以前寫的),如果沒有說明文件,他可能會因為對程式的了解不足而改出新的臭蟲來,為了避免這種情形,他勢必得從程式碼裡面尋找線索,一步步在腦海中將程式的結構慢慢拼湊出來,這得花費許多時間,畢竟對於人類來說,程式碼仍然不是最快速的溝通方式。

McConnell 在他寫的 'Code Complete' 一書中介紹了幾種撰寫程式碼註解的技巧及注意事項,建議您看看。

4. 哪些因素會降低可維護性?

4.1. 不要未經思考就開始寫程式

這道理看來很簡單,但開發人員卻還是常犯這個錯誤。我可以大聲地向你保證,不經過思考(以及寫下你的想法)就開始寫程式絕對是自討苦吃,但即使我喊破了喉嚨,恐怕也不見得有效,因為「僅早寫程式以縮短開發時程」的美麗幻想實在太誘人了。

在接下來的幾個小節裡面,我會把我過去碰到的問題提出來與你分享。

4.2. 不要使用全域變數

全域變數是罪惡的淵藪,只會給程式設計師帶來痛苦而已。看看這個例子:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;

type
  TForm1 = class(TForm)
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

end.

在這個單元裡,變數 Form1 是一個全域變數,因為它被宣告在單元的 interface 區段,這表示每個使用到這個單元的程式都可以使用 Form1 這個變數。這也是初學者覺得 Delphi 容易上手的原因之一,我不是說容易上手不好,它對初學者來說是非常有幫助的,可是在你學習成為大師的旅途上,你應該要知道何時該避免使用那些看起來方便卻容易導致錯誤的方法。

全域變數違反了許多之前提到的原則,它們會增加程式之間的耦合度,而且你無法對它們強制使用間接存取。之所以會增加偶合度,是因為全域變數可被任意單元存取(如果不會被其他單元使用的話,宣告在 implementation 區段裡就行了),每個使用到這個全域變數的單元都會跟宣告該變數的單元牽連在一起,濃得化不開。

無法強制使用間接存取的全域變數將是維護程式時的大問題。怎麼說呢?假設你有一個全域變數叫做 UniqueID,你用它來記錄某個識別碼,當你想要將每一次改變 UniqueID 的動作都做成記錄(log),或者要限定 UniqueID 的值不可為負數時,你就得把所有用到這個全域變數的單元都修改一遍,這實在太沒效率了,根本不可能這麼做。如果你真的非使用全域變數不可,請你改用這種方式:把變數宣告在單元的 implementation 區段,然後撰寫兩個全域的函式,分別用來讀取和設定變數的內容(這和前文提到的屬性的做法很像),這讓你就可以將存取到該變數的程式碼集中在一個地方。參考下面的範例:

unit Unit2;

interface

procedure SetUniqueID(Value: integer);
function GetUniqueID: integer;

implementation

var
  UniqueID: integer;

procedure SetUniqueID(Value: integer);
begin
  // you can add extra code if you want
  UniqueID := Value;
end;

function GetUniqueID: integer;
begin
  // you can add extra code if you want
  Result := UniqueID;
end;

end.

4.3. 確定你有足夠的知識

當然啦,開發人員不了解自己使用的開發工具也是造成維護困難的原因之一,我自己也曾因為這樣而寫出糟糕的程式碼。

在學習的道路上,你應該盡量避免孤軍奮戰。有沒有花錢去外面上教育訓練課程並不重要,重要的是你是否能成為同事眼中最棒、最有生產力的開發人員。

譯註:能夠獨立學習固然很好,但大部分的人卻容易陷入摸索解謎的困頓之中而停滯不前。

4.4. 不要在事件中使用固定的元件名稱

這是個簡單的技巧。如果你在事件中明白指定使用哪個元件,這個事件就很難被重複使用了,例如:

procedure TForm1.Button1Click(Sender: TObject);
begin
  Button1.Caption := 'This is a button';
end;

其中指明使用 Button1,因此這個事件就不能跟其他的按鈕元件共用了,下面的寫法則可以讓多個按鈕共用一個事件:

procedure TForm1.Button1Click(Sender: TObject);
begin
  (Sender as TButton).Caption := 'This is a button';
end;

5. 進階閱讀

以下書籍對於本文的主題有更深入詳盡的探討,而我之所以特別推薦 McConnell 的書,是因為其內容兼具理論知識與實務經驗之故。

Code Complete
A Pratical Handbook of Software Construction

Steve McConnell, 1993
Microsoft Press
1-55615-484-4

中文版: 如何進入程式設計的專業領域 (兩冊)
黃昕偉 譯,旗標出版 (1996)(已絕版

Rapid Development
Taming Wild Software Schedules

Steve McConnell, 1996
Microsoft Press
1-55615-900-5

中文版:微軟開發快速秘笈
鄒正平 編譯,微軟出版社。

 

Software Project Survival Guide
How to Be Sure Your First Important Project Isn't Your Last

Steve McConnell, 1998
Microsoft Press
1-57231-621-7

中文版:微軟專案求生手冊。
余孟學 編譯,微軟出版社。