2007年11月20日

軟體設計的思維

第  1  節  前言

常常有人問筆者:要學什麼程式語言比較好?同樣這批人在學會某種程式語言後,
又常會問說:接下來要學什麼程式語言呢?為什麼學會電腦語言卻又寫不出程式來呢?
相信讀者中必然也有不少人有類似的問題,但可能找不到適當的答案。因此你這時你
不禁會懷疑:電腦到底可以幹啥?

基本上這源自於一個很簡單的誤解,因為不少初學者認為除了瞭解鍵盤如何使用、
電源如何起動外,學電腦最主要的目的是要學會如何寫程式!而不是用來處理資料、
編輯文書......。當然啦!如果有心人想對電腦科學做更深入的研究,當然是樂觀其成
的,但可惜的是入門者常常是在走錯路後才恍然大誤。這不但浪費時間,也相對的浪費
金錢。有鑑於此,筆者希望能透過這篇文章,讓有心大眾能少走些冤枉路,而加速邁向
成功的終點。

第 2 節 你要電腦為你做什麼

為什麼需要寫程式呢?理由很簡單,就是因為你對電腦有所求!希望它能為你作些
事。點子(計畫)的來源可能是前夜周公來托夢,或者是因應他人的需要(例如朋友、
客戶、或是自己)而產生的。你應該詳細的思考一下你對電腦的要求,你希望它作些什麼
事,程式必須有何功能?輸入資料的型式(是圖片、文字敘述、電話號碼......)?輸出
的結果?型式?......越週詳的考慮,對於往後的工作進行會有越佳的幫助。

一般而言,這些雜七雜八的要求可能會顯得十分混亂,特別是由那些不懂電腦的所
開出的規格,更是匪夷所思,摸不著頭緒。而身為工程師的您,你的工作便是分析出程式
的功能。這有點像是室內設計師的工作,你的客戶只可能會告訴你,這裏要擺張桌子,
但不告訴你尺寸;他可能會突然想到椅子上必需要鑲入珠寶,但卻不會去考慮椅子是紅
還是黑的......。因此,充份的溝通是有必要的,軟體工作者應該充份的了解客戶的需要
。當然這點做起來可能很難,特別是當你的客戶是暴發戶型的大老板時,那麼此時必需要
以心理醫師的方法慢慢的誘導出你所要知道的細節。

如果你寫程式的原因是要交作業的話,盡可能的弄清楚文字說明的意義,該達到的
目標一點都不可少,與主題無觀的應予拋開。想一想下面這個問題(這題是七十九年度
程式設計比賽校內初賽的一個問題):

“設計一程式,以之判讀一般之中序算術式,是否為有效或無效之運算式。
式子中所涉及的運算元暫定為由A到Z或a到z的單一字元,可為使用之
運算子則包括加、減、乘、除及括號與次方。程式的輸入為一中序的運算式,
而其輸出為一判讀之結果。”

這邊要特別解釋一下:中序運算式指的是我們一般所列的式子,例如 1+2=3 。
題目就這麼多,條件也說的很明白了。讓我們來整理分析一下它到底要作什麼。

關鍵字句在於“判讀一般之中序算術式,是否為有效或無效之運算式”,說得清楚些
,就是說判斷一個式子是否正確,例如 1 + 2 是對的, 但 1 +* 3 就不對了。另外,
我們必需知道哪些符號是合法的,由“加、減、乘、除及括號與次方”此句中我們知道
可以用“+”、“-”、“*”、“/”、“(”、“)”、及“次方”。前面的符號
都沒問題了,但“次方”就麻煩了,因此電腦的符號系中似乎沒有強制限定次方符號,
例如 BASIC 是以 ^ 為次方符號,FORTRAN 是以 **,甚至C語言根本就沒次方這回事。
此時,儘可能的與試務人員交換意見,或者明白的在你的說明文件中定義次方符號。但
因為這場比賽是以 BASIC 撰寫的,因此便以 BASIC的 ^ 表示了。接著,在“運算元暫定
為由A到Z或a到z的單一字元”此句中我們知道一個算式必需以英文符號代入運算,
例如 x=y+z,但相對的,23+12 就是不合語法的了。並且,題目中只要我們辨別出運算式
是否正確即可,不必畫蛇添足,算出到底是多少來。另外題目中並未提到“運算式的來源
”,究竟是經鍵盤輸入或是由檔案中取出呢?但經瞭解後是由鍵盤輸入。而以上就是對
有限條件題目的分析。

在你仔細的分析完這個問題後,你可能會發現解決這個問題對你而言可能是力不可及
,或者是所需時日甚多,無法如期完成,或是有現成的套裝軟體可以立刻解決此需求的...
... 而無論如何,這就是我們所以要分析定義問題的緣故。

第 3 節 仔細的思考軟體的結構

經過一番努力後,你已經定出一套完整的規格,此時你大概也有個譜了。然而這只是
第一步罷了,系統結構尚未建立呢!因此別興奮的太早,接下來你應該要把夢想落實,
一步一步的具體化。規劃的目的除了在描繪具體的軟體結構外,重點是要進行模組的建立
,資料格式的規劃,模組間的通訊協定(Protocol)等工作。

關於規畫軟體,有很多理論上的設計方法。但有諺曰:『一圖勝過千言萬語』。畫圖
是幫助我們規畫程式的最佳方法。幾個比較著名的方法,例如流程圖、拿西-尼得門圖
(Nassi-Shneiderman)、及下面要談的 DFD 圖。

第3.1節 DFD圖

一般的軟體,最基本上可分為“輸入”、“處理”、“輸出”三部份;所謂的“資訊
”,其實所指的就是經過處理後的“資料”,因此DFD圖便應運而生了。DFD圖的
精神就在於資料流程的規劃、各處理單元的動作、並且可以作更深一步的切分。DFD圖
有幾個符號,介紹給大家認識一下:

┌──┐
│ │ 外界個體 系統輸入的起點或輸出的終點
└──┘

╭─╮
│ │ 轉換程序 執行輸入資料處理轉換成輸出資料的單元
╰─╯

╭───→ 資料流 用以連接不同的程序,以表示資料傳送的方向


──╮ ╭─→
↓ │ 資料儲存所 資料儲存的地方,箭頭表示資料的來源及輸出的方向
═══════


舉一個銀行自動櫃員機的 DFD 圖:

┌────┐ 提款卡密碼 ╭───╮ 付款訊息 ┌──┐
│ 終端機 │──────→ │櫃員機│ ────→ │點鈔│
│ 鍵盤 │──────→ │ 系統 │ │付款│
└────┘ 提款金額 ╰───╯ └──┘

《第 A 層次的 DFD 圖》

這是最初步的,未經細部規劃的。但一個雛型已經產生了。下一步工作是逐步的將
DFD 圖分解成細節。

┌────┐ 提款卡密碼 ╭───╮ 帳戶資料
│ 終端機 │─────→│總行電│←─────╮
│ 鍵盤 │──┬──→│腦查核│────╮ │
└────┘ │ ╰─┬─╯ 帳戶 │ │
│提款 │查核 │ │
│金額 │結果 ↓ │
│ ↓ ══════
│ ╭───╮ 銀行的輔助除存裝置
╰──→│櫃員機│ 例如磁帶機
│ 電腦 │
╰─┬─╯
│ 付款訊息 ┌──┐
╰──────→│點鈔│
│付款│
└──┘
《第 B 層次的 DFD 圖》


┌────┐ 提款卡密碼 ╭───╮ 付款訊息 ┌──┐
│ 終端機 │──────→ │櫃員機│ ────→ │點鈔│
│ 鍵盤 │──────→ │ 系統 │ : │付款│
└────┘ 提款金額 ╰───╯ : └──┘
. ...
. ...
. ....
. :.
. ╭───╮ 帳戶資料 .
提款卡密碼 │銀行電│←─────╮ .
─────→│腦查核│────╮ │ .
─────→╰─┬─╯ 帳戶 │ │ .
提款金額 │查核 │ │ .
. │結果 ↓ │ .
. ↓ ══════ .
. ╭───╮ 銀行的輔助除存裝置 .
. │櫃員機│ 例如磁帶機 .
. │ 電腦 │ .
╰─┬─╯ .
│ 付款訊息 .
╰─────────→

《資料流程的分解》

第3.2節 由上而下的程式設計 (TOP-DOWN DESIGN)

由上而下的設計哲學,是一種逐步細緻化的設計概念。就像你寫生時,必然是先打
草稿,既而以大號畫筆勾勒輪廓,再用更細些的筆描繪出細部構造。很少有人是拿細筆
一筆一筆刻劃的,即使有,也不容易畫得好。可能的結果是畫虎反類犬。有個人找了位
建築師幫它設計一幢洋房;數週後,他去找建築師看看設計圖。於是建築師便拿了張圖
告訴他說:“讓我們來看看廁所屋頂的水管設計圖”。“可是我還不知道房子到底是什麼
樣呢?”顧客說。誠所謂綱舉目張,當結構設計好之後,細節就可一一浮現了。並且,
由上而下的方式,不僅運用於初期的規劃,也應用於編碼 (CODING)、測試時。

一般而言,一個完整的程式是具有“層次性”的。大部份看來就像例圖這般:
(當然實際情況會複雜多了)

┌───┐
│主程式│
└─┬─┘

┌─────┼──────┐
│   │   │
│   │   │
│   │   │
┌───┴─┐ ┌─┴───┐┌─┴───┐
│主要模組A│ │主要模組B││主要模組C│ .....
└─────┘ └──┬──┘└─────┘

┌───┴┬─────┐
│ │ │
│ │ │
┌─┴───┐
│次要模組D│...........
└─────┘
(模組在下一節介紹)

因此我們對一個程式的結構應該是由頂端主程式開始思考,再逐步的往下深入,像
蓋房子要先打地基,再建一樓,再建......如此一層層的堆上的。在作由上而下設計時,
要避免“向下思考”,也就是說先不要考慮下一層模組的工作。並且也不要在規劃時期就
急著編碼。不然何必先規劃呢?

同樣的,當程式在編碼 coding 時,則能夠先寫上層的模組,再繼續往下寫。寫好
一個上層模組後,可以先進行模擬測試,完成後再繼續下一步驟的工作。

不過由上而下的設計、編碼、測試都只是大原則罷了。一個專業的設計師真正在工作
時可能也會由下而上的設計、編碼、測試。為什麼呢?因為我們可能會須要一些低階的
副程式,例如視窗副程式、繪圖驅動程式、通訊程式等等,不過要注意的是,這種由下
而上的寫作方式依然還是得在由上而下的訂定整體結構後才實行,不然常常會發生上下
模組無法銜接的情況,這就像開挖隧道一樣,你可以分兩組人馬分別從兩端開挖,但精密
的測量和協調絕對是必需的,否則將會出現兩條隧道......。

第3.2節 結構化的程式設計

將到結構化,很多人便會想到“goto”敘述的存廢問題。理想中我們的確不希望
見到goto的存在,但對於那些老舊的程式語言而言 (BASIC, FORTRAN, COBOL...),
去除 GOTO 敘述是幾近不可能的!

那麼到底結構化程式設計的本質是什麼呢?在結構化設計的理論中,任何一個程式的
流程都可以被分類為三種形態:循序、二元分歧、迴圈。也就是說經由這三類的基本指令
就可以構成一整個程式。
 
│ │ │
│ │ │
↓ ↓ ↓
┌────┐ /\ /\
│ 處理 │ / \ / \
│ 方塊 │ 〈 判別 〉 ┌→〈 〉
└────┘ \  / │ \  /
│ \/ if │ \/
│ │ │ │
↓ │ │ │
┌──┴──┐ │ ↓
then │ │ else │ ┌────┐
│ │ └─┤ │
↓ ↓ │ │
└────┘

循序結構 選擇結構 迴圈結構

另外,整個程式的流程是極順暢的由起點到終點,而不是用 goto 指令改變程式的
流程。結構化的程式在閱讀上十分容易,因為閱讀者不必跳來跳去閱讀,而是一目瞭然
的!下面附的是一個用C++所寫的範例,讀者可以感受一下她的優美性:

int main( int argc, char *argv[] )
{
if ( argc != 2 )
{
cerr << "Usage: lookup classname\n" return 1; } Dictionary& classDefinitions = *( new Dictionary ); for ( int i = 0; i < classname =" *(" classdef =" *(" entry =" *(new" definition =" classDefinitions.lookup(" definition ="="" style="color: rgb(0, 0, 255);">第 4 節 資料結構與演算法

最近看到不少人都在參加〞電腦擇友〞的活動。參加這活動的男男女女將條件填在
表格上,寄給主辦單位的電腦,處理後將形成〞最佳〞的配對資料。但筆者要偷偷的告訴
你們,要換成是筆者,就根本不會期待這個結果是正確的!當然啦!如果有人因此而陷入
熱戀,那我們應當祝福他們,但請不要告訴他們說:有個姓林的電腦狂說電腦擇友是唬人
的!

諸如電腦如何由眾人中依條件找出配對的方法,或是電腦鼠走迷宮,事實上都是按照
一套已經預先建立好的方法來進行,此便稱為演算法。廣義的演算法解釋是這樣的:凡是
一個程式的執行,必然是按照一定的規則步驟去進行。整個程式的流程便是一個演算法,
但一個大的演算法可以是(或必然是)許多演算法個體的結合。但通常我們常常會討論
一些特殊用途的方法,例如剛剛舉的電腦擇友,數字從小到大的依序排列,乃至於計算機
的微分解法,都可歸類為狹義的演算法。而資料結構與演算法則是一體的兩面。正確來說
是密不可分的,更保守來講演算法就是資料結構,資料結構就是演算法!但若要筆者來分
的話,筆者則將資料結構定義為一個“資料的抽象描述方式”,而演算法則是“處理資料
的方法”。

“條條大路通羅馬”這句話可以說是程式設計千古不朽的銘言啊!因為解決一個問題
的方法必然不會是唯一的,舉例說從台灣到美國,你可以考慮坐飛機,也可以搭船,當然
啦,如果你自願游泳渡過太平洋筆者也不反對。但是“效率”與“實行成功性”便成為
我們必需好好考慮的要素。坐飛機無疑是最有效率的選擇,但卻不適合大量貨物的運送,
因此此時你便得考慮船運。如果你沒錢買機票或船票,那麼游泳可能便是你最後的抉擇了
......。選擇正確的演算法,會使程式的效率大為提高,並且可以確保程式是否能正確
完美的執行。並且對於某些特殊問題的解答將會獲致意想不到的效果。

其實整個資訊科學可以分成兩大類別﹕一是實務派,也就是真正投入工業生產的,
把演算法實際“實作”的﹔另外一類是學術派的,他們的工作主要是研究規劃特定的演算
法,但兩者並非相衝突的。一位好的程式設計師通常也應該是一位好的演算法設計者,
否則他也必需要會正確的引用已知的特定演算法。但很多〞玩家〞倒不重視這些了,他們
總以為程式會跑就好了!這是一個很危險的想法,也是不正確的(但這也是專家和玩家的
分野)。理由就像上面所講的。

不但設計演算法是一們學問,實作演算法(編碼)更是一門大學問!一個好的編碼
結果必然會有較佳的執行效率,例如“壓縮程式”(像 LHARC, PKZIP, PKARC 啦...)
其實大部分用的都是相同的演算法,但其壓縮後的縮減比例和計算速度卻都有很大的差別
,因此演算法實作的重要性自然不在話下。

由於這篇文章不是討論資料結構與演算法的專書,筆者的目的是在給各位讀者一個
演算法上的概念。但相信藉由舉一些著名的演算法為實例必然可以引起讀者較高的興趣。

排序
──

排序法幾乎是每一位學電腦的必修的項目。方法很多,筆者舉兩個極端的例子,它們
分別是“泡沫排序法”和“快速排序法”。假定有一堆散亂無序的數字:(由小而大排)

23,64,2,73

若是用泡沫排序法來解決的話,它會這麼做:依次比較一對數字,如果後者大於前者
,則互換排列;用這個方式從頭到尾循環進行,直到沒有交換的情況發生才算完成。

執行過程:

┌──┬──────────────────────┐
│步驟│排列情況 │
├──┼──────────────────────┤
│ 1│23,64,2,73 第一回合 │
│ │^^ ^^ │
├──┼──────────────────────┤
│ 2│23,2,64,73 │
│ │ ^ ^^ 大小互換 │
├──┼──────────────────────┤
│ 3│23,2,64,73 因為發生過交換,所以必須繼續執行 │
│ │ ^^ ^^ │
├──┼──────────────────────┤
│ 4│2,23,64,73 第二回合 │
│ │^ ^^ │
├──┼──────────────────────┤
│ 5│2,23,64,73 │
│ │ ^^ ^^ │
├──┼──────────────────────┤
│ 6│2,23,64,73 並未發生交換 │
│ │ ^^ ^^ │
└──┴──────────────────────┘

另一個解決排序問題的方法是“快速排序法 Quick Sort“,相對於泡泡排序法,
Quick 的效率有明顯的改善。假定有 n 個數字等待排序,若以泡泡排序解決,理論上須
費時 n 平方的基本時間單位,如果用快速排序法來做,理論上所花費的時間大約是 n 的
1.2 次方左右的基本單位時間。當然實作時會有很大的出入,不過,事實很明顯,採用
快速排序法的確能節省較多的時間。快速排序法是如何動作的呢?以一個較簡單的概念
來解釋,同樣是 23,64,2,73 這個數列,Quick 的作法是這樣的:

(A)
取一個數 m 為基準,使得剩下的數字可分為兩堆,第一堆的任一個數字都小於
等於 m,另一個數列中的任一數字都大於 m。
(B)
將 (A) 步驟所分離出的兩個新數列分別再以同樣的方法分割,直到不能再分割
為止。

也就是說,QUICK 採用的方法是一個“各個擊破”的方式。

第4.1節 選擇適當的方法

如前面所提到的,QUICK SORT 的效率高於泡泡排序,但這便表示我們在各種需要
排序資料的場合都應毫不考慮的應用 QUICK SORT 嗎?答案並非肯定的,很簡單的道理:
如果它真是那麼沒用的,它並沒有存在的需要!換句話說,泡泡排序在某些場合可能有用
。而這也是本節討論的重點:適時、適地的選擇適當的演算法。

設計程式時常常必需要分析我們所要處理資料的特性,而採取適當的演算法,換句話
說,程式設計師應該要瞭解資料與演算法的特性。就以上述的泡泡與快速排序的問題來看
,泡泡排序的執行時間與資料個數的關係呈現出一個指數圖形,而快速排序則呈現一個
對數的關係。如圖所示:
︿   ︿
時間│ + 時間│ ********
│ + │ ***
│ + │ **
│ + │ *
│++ │*
└──────> └───────>
個數n 個數n
泡泡排序法 快速排序法

這表示:當要待排序的資料個數n小於某個範圍時,泡泡排序的效率可能會較快速
排序來得快。例如以統計學生成績來講,統計一個班的成績便適用泡泡排序法,因為通常
學生都在五十人上下;但統計一整個年級的成績就適用快速排序了,因為數量極大。

由上例我們便知道演算法對程式執行效率的影響。在電腦科學界中,有一個最廣為
人知的定理便是『空間與時間』的相對關係。在一個理想的環境中,當你在選擇演算法及
資料結構時,便需考慮到:到底是花多一點時間來處理資料還是用較多的記憶空間來爭取
較快的速度呢?舉一個明顯的例子,就以『串列』來說,單向串列的每一個節點僅須要
記住指向下一個節點的位址值(當然也可以指向前一個),而雙向串列則須記住前後兩個
節點的位址,因此雙向串列便較單向串列需要更多額外的記憶體。但有得必有所失,雙向
串列在做“巡視(TRAVEL)”動作時較單向串列便要快了。理由是因為“回溯(reverse)
”節點時,必需從頭計算前一個節點的位址。另一個則可舉前面所提的排序法,做“遞迴
式快速排序”時要耗去不少堆疊的空間,甚至有可能造成堆疊過滿的情況,當然解決的
方式不會是採用泡泡排序的,通常我們都會應用非遞迴的 quick sort 來作。另一個在
生活中常見的實例便是發生在碟式中文系統上,你要是設定較少的字型緩衝區,相對主
記憶體會有較多空間可用,但顯示或列印速度便被犧牲了;反之設定較多的緩衝區供中文
應用,顯示速度固然加快了,但剩餘的可用空間反而少了,這個結果常常導致很多套裝
軟體無法執行。

因此在設計程式時應該考慮將重點放在節省時間還是節省記憶體上。假定你設計一個
在中文系統下執行的軟體,那你可能必需要盡力的節省記憶體了,因為碟式中文系統常要
花去不少記憶空間。但假使你正在設計一個飛機場的航管系統,那便務必求快,否則空難
的發生將會是家常便飯的事......。

另一個選擇演算法所要考慮的要件是“可讀性”的問題。固然選擇又快又小的演算法
是我們的目標,但各種演算法中,不乏高度數學化、不易為人瞭解的方法。例如字串的
搜索法就難倒不少工程師了。想從這篇文章中找出『演算法』這個字串有什方法呢?一般
人類直覺上會先比較第一個字,相符後再比第二個字......重覆這樣的步驟。這種很笨拙
的『暴力法』的確不太好。有個方法叫 K.M.P., Kunth - Morris - Pratt 法,它的工作
方式是將要比對的字串中每個字元的相對位置,再決定比較失敗時要從那裡繼續工作。
另外還有 BM 法、RK 法,說實話並不容易了解,況且效率不會說有很高的差異(其中
RK法要用到大量記憶體)。因此很多工程師便乾脆繼續用他的暴力法,而捨棄快、但
不易瞭解的方法。理由是因為這些方法可能造成測試上的困難。另外要程式設計師對於
一個不瞭解的演算法加以實作,可能不能得到很好的編碼(當然這和設計師的經驗有關)
。所以便有人認為寫程式應該採取 KISS 的規則來,KISS 是 Keep It Simple and Stupid
之簡稱,而不是 kiss 的意思,就是讓程式的流程簡單而且“樸拙”,這點和我們的校訓
『勤樸誠勇』好像是相通的。

第4.2節 效率的考量

提到演算法,就不得不讓我們考慮到效率的問題。一般對效率的定義是指完成某件
工作所需的時間、空間比。我們可以從幾個方面來討論。

(一)與機器的影響

程式的執行效果在不同電腦上的執行成果,差異是顯而易見的,而改善硬體環境則是
提升軟體效率最明顯的途徑。這也是人類為什麼致力於製造更高速的電腦。例如氣象局
便需要超級電腦,因為明天的天氣總不能等到後天才算出來吧!無論如何,軟體還是應該
盡力地提升本身的效率,而不能依恃硬體的進步。否則仍無法發揮整體應有的效能,甚而
形成系統的浪費。

(二)與程式語言的差異

程式語言對於軟體效率甚至目標的可行性究竟有多大影響呢?這點在計算機科學界
一直爭論不休。但一般而言,在同樣的結構、相同的演算法,用不同的程式語言所編出的
程式大概會有15%~20%的影響。這是不是說用組合語言寫出的作品會有較高的效率
呢?非也非也。良好的計劃直接影響了編碼的品質。同樣的模組若同時以組合語言和高階
結構化語言撰寫,一般的結果通常告訴我們採取組合語言實作將發生悲劇!組合語言通常
會多出1/3的花費(花費指的是用了多少記憶體和時間)。科學家發現“編譯程式”的
好壞才是整個問題的關鍵。現今結構化的編譯器對於“最佳化”的處理已經是相當的優秀
了。各位如果手邊有 Turbo C 2.0 和 MSC 6.0 編譯器的話,不妨自己做個比較,你便會
有驚人的發現。同樣的你也可以要求編譯器產生組合語言程式碼,看看編譯器的處理手法
是不是比你用組合語言撰寫來的高明。但專業的應用上,我們常是用高階語言寫好原始
程式,再用組合語言作低階的最佳化,例如繪圖函式這類對速度要求極高的部份。

但考慮一下“維護”與“除錯”,結構化程式便較非結構化程式語言甚至組合語言
來得容易的多了!況且,每個程式必然都會發生錯誤!如果因為求一部份模組的“技巧性
”,而導致整體工程的延宕,是很划不來的。

(三)與演算法的影響

事實上,演算法對於整個程式才具有真正決定性的效率影響,理由在上一節就說過了
,為了避免讀者忘記,請再參考前面的理由。

(四)與軟體結構的影響

與其說是與軟體結構的影響,倒不如看成整個系統流程的合理性。事實上整個資訊
科學所關心的,不僅僅只是程式該怎麼寫,系統運作的方式也是我們所關心的。因為我們
或是一些機關在處理事情的流程和做法上,常常有重覆而不合理的地方。這種情形或許在
人工作業時期可以諒解,但在電腦化的時代只是多餘的浪費罷了。例如說圖書館的書目
管理,一本書至少須要書名卡、作者卡,還有書籍本身的出借紀錄,而借書人的借書證上
還要登記這個人借書的紀錄。各種資料實在有夠多也有夠雜,但如果在電腦上,你還用
這種方法來為每一本書建立各種檔案,真是浪費記憶體的行為。事實上書籍只要一個資料
檔就可以解決了(理想中),借閱人要查詢資料的程序也簡單多了;借書證上也不必填
一堆東西,一切都是由電腦來整理歸類資料。換句話說,系統的合理性,是影響整個軟體
最基本的變因。

第 5 節 程式設計


第5.1節 程式語言

程式語言依照年代和特性的先後可以分成四類。各代有各代的特色,也因此在軟體
設計方式上有不同的方法:

第一代 組合語言/機械語言

最原始的程式只是一堆 CPU 所能瞭解的指令集,它們都是數字,不過實在很難瞭解,
像“23 EA 73 66”這樣的數字代表什意義呢?除了 CPU 和天才外,恐怕沒人能瞭解,
因此我們便用一些簡字符號來代表某個機械碼指令,例如 80X86 的 MOV、ADD 等等。
它的特點便是非結構化、極低階的運算方式、但執行速度極快,不過很難為人了解,因此
在設計上常常造成很多困難, 不過許多較好的編譯器都會提供巨集 MACRO,提供較佳的
設計途徑。

第二代 非結構化語言

這代語言的黃金時期是在 1960 和 1970 年代。這時代的作業系統正在發展分時系統
,硬體環境相當嚴苛。BASIC、FORTRAN、COBOL.....都是這類語言的代表。他們的特色是
非結構化,也就是說 GOTO 用的很多,此外缺乏模組化的結構。大家可以看看 BASIC 是
什樣子的,大概也就差不多了。

第三代 結構化程式語言

第三代程式語言起於 1960 年末。這代語言都具有嚴密的區塊結構、更抽象的資料
封包方式、結構化的語法結構、適當的資料形態、程序與函數的結構相當完整。Pascal
是最具代表性的程式語言,事實上它當初設計的目的便是指導人們以結構化的思考方式
來寫作程式,也因此成為研究計算機科學者必修的語言。C語言一直是我的最愛,它本來
是貝爾實驗室用來發展UNIX作業系統的,但目前許多應用軟體也用C語言來寫(例如
dBASE)。C語言不但具有像 Pascal 的結構,它本身具備了一些低階的特性,五十餘個
強有力的運算子,使得這個語言的結構更是完美。

第三代語言還有兩種較特別的族類,一個是人工智慧 (AI) 用的程式語言,另一種是
物件導向程式語言 (Object Oriented Programming Language, OOPL)。人工智慧語言可以
用 LISP 和 PROLOG 為代表。這些語言的特點在於它們對符號處理能力很強,PROLOG 對於
階層性的串列還可以自動回溯。OOPL 則應以 SmallTalk 為代表。Smalltalk 和C一樣也
是貝爾實驗室的產品。它允許我們將有關的資料與程序(OOP 術語叫方法 METHOD)捆成
一包“物件 Object”,相類似的新物件可以經由“遺傳”舊物件而來。整個 Smalltalk
就有三四百個庫存的物件,一位 Smalltalk 的設計師只要遺傳這些物件就可以了。最近
漸趨熱門的 C++ 則是在這股 OOP 風潮下,由原先的C語言加強而成的。

第四代程式語言 4GLs

第四代程式語言又較第三代來的高階並且更抽象化。以前程式設計師必需費時於設計
很多演算法。但經由第四代程式語言,設計師只要描述它所要的需求(requirement),
就可以輕易地設計出程式來,例如 dBASE 等。不過目前大部份 4GL 都用在資料庫或商業
方面,並不易運用於其它的範圍,例如系統程式的設計。不過等將來人工智慧的技術發展
成熟了,相信 4GL 必然會成為主流。

筆者強烈地推薦以第三代以後的語言來設計程式,第三代語言的好處在於它能夠很
簡單將各類演算法實作出來,並且又具有易於除錯和維護的優點。第四代語言則可以節省
大量的時間在細節的設計上。第二代語言如 BASIC 或 FORTRAN 要真和 Pascal 或C比起
來,實在是很可憐的。

第5.2節 考慮系統

筆者認為有時真實世界的作業環境對於程式設計者常常會是一場夢魘。此話怎講?
因為我們常常看到教科書上不斷的提示說:降低程式對於電腦(機器)的依賴性。這句話
實在說得很曖昧。假如你今天設計一個在PC上使用的電動玩具,你將會發現各機器的差
異是如此的大,光顯示器常見就四、五種了,還有各家音效卡也不一樣,透過 BIOS 來作
繪圖的工作似乎是很愚笨的一件事,因為像 F-19 這類模擬飛行的遊戲玩起來將會像騎三
輪車一樣缺乏速度感!與系統的相依性越低,可能就必需降低不少效率!這真是一個讓人
傷腦筋的問題。我們實在不得不承認﹕一套軟體的發展的確受限於電腦硬體環境的限制。
筆者提出一些解決的參考方案:將與硬體高度相關的部份獨立成模組;在規劃程式的當初
最好考慮將要採取的系統或是周邊裝置。不過,仔細想想,大部份程式實在不是很有必要
依靠機器,頂多也是一部份罷了!

另一個 PC 使用者常見的問題是在於『編譯器』的問題!特別常見於用TC的人身上
。雖然程式語言幾乎都有國際標準的語法(例如 ANSI, ISO 等),但移植到各種電腦、
作業系統、編譯器上就會出現『方言』,這導致程式原始碼(source code)的可攜性降
低。這就像是湖南老鄉見到山東大漢一樣,雖然講的都是中國話,但溝通上實在有困難。
解決這個問題有兩點建議:(一)當你開始寫程式時,就必需按標準來寫,即使有部份要
用到方言的語法,必須分開獨立並且要詳細的指明。(二)下定決心,將終身大事托付給
你的編譯器吧。


第6節 撰寫程式的風格

程式設計本質上至少有一半具有藝術創作的特性,好的程式往往就是一件藝術品。
而你,電腦藝術家,具有你自己的風格,你自己的創意,因此你的作品將會處處顯露出
你獨特的個性。

當然這麼說並不是要你將程式『畫』得像畢卡索的作品一樣,但至少你的程式應該
具備一些特性:明確的定義、詳盡正確的註解、善用縮格與空白等等。當然你可以培養出
你特殊的氣質,展現在你的程式中。

註 解
───

註解是說明程式碼內容的文字,明白而清晰的註解使得程式碼容易閱讀與瞭解,
很多人多懶於撰寫註解,他們常見的藉口不外:

我的程式本來就很容易瞭解了
我當然看得懂我自己寫的程式了

諸如此類都不過是推拖之辭罷了!人們常常忘記一些瑣碎的問題,這當然也包括他們
自己的程式在內。到底這個函數的參數代表什麼意義呢?他的傳回值又是什麼?螢幕的
左上角是(0,0)還是(1,1)? 這些都應是註解的內容之一。但我們須要寫多少
的註解?註解的內容又該有多詳細呢? Planger 說:『好的註解不能代替壞的編碼,
然而好的編碼是否就能夠取代註解,那就不得而知了。』爛的註解就像撒旦一樣,會給你
錯誤的指引。通常的規則是把註解分兩類來寫:前言性註解(prologue comments)和功能
註解(functional comments)。前言式的註解一般是放在模組的前端來指明該模組的技術
簡明資料,大概包含了:

(一)功能和目的
(二)介面說明
A. 說明參數意義
B. 說明傳回值的意義
C. 所需參考到的模組
(三)資料的規格和限制
變數的使用方式和範圍,例如標明螢幕左上端是 (1,1)....
(四)發展紀錄
誰設計了這個模組,修改的日期和所修改的內容等等歷史資料

功能性註解則是安插於程式碼中來說明某一區塊程式碼的功能。寫註解並不是寫文章
,不必寫的長篇大論,但也不要太簡略到用幾個簡字符號。有部份註解的內容可能是在
除錯時留下來的,這些註解最好在除錯完成後便去除,以免造成誤解。另外關於註解要用
英文還是中文來寫,筆者倒認為“易懂”才是重點,用中文不一定會比較爛。

變數的命名
─────

常常看許多人用了一些毫無意義的變數名稱,像 A,B,C,D,X,Y,Z,PP,SS,TT..
亂七八糟,而這種情況特別常見於初學者。這種習慣不是很好,甚至比那些沒寫註解的
差上一級,當然要是註解沒寫,變數名稱又亂寫的,真應該好好反省了。變數的命名應該
以明白易瞭解為主。許多人為什麼用無意義的符號來做為變數名稱的原因是因為嫌多打
幾個字太累,或是佔空間等等。不過還有一種是較誇張的行為,就是變數名稱寫的有夠長
,例如:AmountOfYear、KiloMeterPerSecond等等,的確是有點矯枉過正,不過還是比
那些用無意義簡字符號來的好。在為變數命名的當初,最好能建立一個『字典』來說明
縮寫與展開字的關係。例如:

KM = Kilo meter = 公里
Sec = second = 秒
rec = record = 紀錄

當然最好能夠將該變數的意義附加在表格後面,建立一個『變數字典』,方便程式
設計工作的進行。

縮 格
───

在結構化的程式中,縮格可以很明顯的分別出每一個區塊,和各區塊間的關係。
例如說:

.... ....
..... .....
if 邏輯判斷 then if 邏輯判斷 then
begin begin
..... 敘述 ..... 敘述
.... ....
..... .....
for 迴圈控制 for 迴圈控制
begin begin
..... .....
....敘述 ....敘述
..... .....
..... .....
end end
.... ....
...敘述 ...敘述
end end
.... ....
....... .......

有『鋸齒』狀的縮格 缺乏『鋸齒』狀的縮格

縮格與不縮格的效果相當的明顯,你要縮多少格都沒啥關係,現代的編譯器都會自動
辨識,但是對於某些程式語言有『欄位』限制的,例如 FORTRAN 等等,會稍微麻煩點,
但可以盡量的作出這個效果來。另一個焦點是在於像 begin 和 end 這一類“標示區塊”
起迄點的位置。以C來說,它以{ 表示 begin,以 }來象徵 end,結果筆者看到的寫法
就有一大堆:

┌───────┐┌──────┐┌───────────┐
│for (......) {││for (.....) ││for (.) { .... .... } │
│ ..... ││{ ││ │
│ ...... ││ ..... ││ │
│ .... ││ ....... ││ │
│} ││} ││ │
└───────┘└──────┘└───────────┘
<<幾種C程式的縮格>>

不勝枚舉啊!筆者是喜歡用中間那個方式,有些教科書則是用第一種,第三種則常見
於只有一行敘述的函數中,這種情況在C++的教本中特別常見。用縮格時不要擔心這是
否影響整個原始碼的長度等等,這是多餘的。較長的原始碼不會導致程式的效率降低,
但亂七八糟的原始碼是鐵定會出問題的。

運算式
───

從兩個方面來看一個運算式的寫法﹕第一是運算式本質上的複雜度,第二是表示的
方法。以第一個角度來看,這樣的式子:

x = 26 * ( 22 + y * 7 ^ z + ( 164 / 64 mod 7 ) )

實在不太理想,因為太多的括號和不明顯的本意讓人讀起來很辛苦,並且,沒有經過
最佳化處理的式子也會降低不少效率,例如說:

x = 2/3 + 5/3

在這個式子中,電腦必需做兩次的浮點數(簡而言之就是帶小數)的計算,分別是
2/3=0.666666 和 5/3=1.666666,但如果你寫成 x = ( 2 + 5 ) / 3 ,只需做一次
7/3=2.333333 的浮點計算了,而電腦作整數計算遠較浮點計算快,這樣寫效率便提高了。

第二個觀點是表示的方法。特別以C語言為例是因為它所有的運算符號是眾語言中
數一數二多的,例如以下的邏輯判別式來說實在有夠曖昧:

!x == 3 && y != 6

但是加了括號後就明白多了:

( (!x) == 3 ) && ( y != 6)

善用括號和空白是很重要的!

第7節 除錯與測試

除錯常常是設計師頭痛的來源。每個程式幾乎都不免有錯,不論是生手或老手的作品
都一樣。我們在設計程式之初總會有美麗的憧憬:當我作好規劃,然後編碼,再修改一些
『小錯誤』,這個程式就完成了!事實不然,或許你寫程式的速度的確很快,但你永遠也
無法知道你將會花多少時間和精力在除錯上,這真是一個可悲的事實!另一方面,『除錯
』這件工作即使是在科學昌明的今日,也缺乏一套好方法,和一些好工具來協助我們除錯
,雖然新的除錯工具不斷的在出現,但除錯依舊還是一件令人沮喪的工作。

一般而言,錯誤被區分為語法上的錯誤和邏輯上的錯誤。語法錯誤的原因主要來自
打字錯誤或一些無意義的語法,較嚴重的可能是指令或變數名稱的誤用,不過這種錯誤
一般而言都不太麻煩,絕大多數語法上的錯誤都會由編譯器偵測出來。因此我們較關心
是邏輯上的錯誤。

在講除錯前先說說測試。測試和除錯是兩碼子事,測試工作主要是在運用一些假設的
狀況,來查驗程式是正常或有錯誤,因此測試的方式和測試的樣本就很重要了,因為沒有
正確的發現程式有錯,問題將會在日後如山洪一樣的暴發出來。當測試工作一開始進行,
並不會斷然的立即測試一整個系統是不是正常,因為你一定會發現有錯,但找不出錯誤在
哪,因為程式實在太大了。一般我們會先進行模組測試,等一個個的模組都驗證無誤後,
系統或許也應該沒問題了。測試程式的方式有主要兩種策略:黑箱 (Black box) 和白箱
(White box) 兩種測試方法,互有利弊。

白箱
──

白箱測試的方式就好像拍攝模組的X光照片後,再仔細的分析。有四個主要的測試
方向:(一)至少將模組中所有的獨立路徑測試過一次。(二)測試所有邏輯判斷的流程
。(三)以『邊界值』來測試模組,例如對於位元組的資料型態,則以邊界值0和ffh
來測。(四)測試內部所有的資料結構。簡而言之,白箱測試就是將模組的內容作執行
路徑的分析,因此模組就像是玻璃屋一樣,一覽無遺。理論上白箱測試應該每一條路徑
都須檢查,但實際上卻有遺漏的可能,特別是在大型的模組中,白箱測試的工作更是複
雜,不過白箱測試仍是必需的。

黑箱
──

黑箱測試用來判定程式是否正確的作了我們要它作的事情。經由這個測試程序,可以
驗證程式是否達到我們的要求。例如說我們要測試一臺“換鈔機”,你可以分別投入各種
幣值的鈔票,換出對應的零錢出來。這個時候,用來測試的數據就很重要了!因為你的
測試數據不可能包含將來所有可能發生的情況,但你卻要保證測試能找出所有的錯誤!當
然這似乎是不可能辦到的。所以我們可以拿以前真正實際應用時的數據,例如說拿百元鈔
換十元幣,五百元換五角等等。拿實際數據來測有個好處,就是常見的正確情況和常見的
錯誤情況都包含在裡面了,因此可以證明程式在真實情況下的可信度。當然你也可以產生
隨機資料,不過這些數據可能就不會常見了,例如說拿一億元換一角的情況並不太可能
發生。

黑箱測試和白箱測試看來似乎是相反的測試方法,前者是藉由測試的資料和結果來
驗證,後者則是仔細的查驗程式中每一個步驟。兩種方法都有存在的價值,因此它們的
效用是互補而非互斥的。白箱測試通常應用於程式設計初期,黑箱測試則應用在軟體完成
後。

錯誤是可以分類的,特別是可以用出現頻率來分別,一直發生,常常發生,偶而發生
,甚至發生過一次後就再也不發生的錯誤。而搜索錯誤的方式也有一些常見的方式:
暴力法、往前搜尋法、原因消除法。暴力法是最常見、最沒效率的方法,除錯的人總是
一步一步的從頭追蹤程式,把記憶體中的變數甚至檔案的內容都印出來,或是在程式中
很多地方顯示執行時的情況......當然這種方法可以找到錯誤,但需要很長的時間和很多
精力,不過我們還是會運用這個方法,特別是在一切都絕望的時候。
   
往前搜尋法或許是較好的方案。從錯誤發生的地方往前找看看到底那裡不對勁。在
程式較小的場合或許管用,但程式太大了就困難了。最科學的當屬原因消去法。我們有
系統的去分析一個錯誤發生的原因,作一些可能的假設。就像是汽車發不動了,你不會
急忙的把車子支解,而是會想想看那些零件的故障會導致車輛無法發動,例如火星塞、
分電盤......。

以上提的三種除錯的方法都有適當的軟體工具來配合,例如單步追蹤器,或中斷點
除錯......等。

不可否認的,除錯的工作是一件令人痛苦而不耐的工作,特別是對於原程式設計者
而言,要他承認自己的程式有錯,真是一件丟臉的事,但事實就是如此,錯誤就在你的
程式中。因此從人性面來考量的話,除錯的工作或許該交由別人來做,但有誰會比設計者
本人更了解這個程式呢?話說至此,筆者還是認為設計師本身應該能夠坦誠的面對問題,
並且不要蓄意的去保留自己的秘密,這點應該是很重要的。

第8節 結語

軟體設計是件高度創造性的工作,因此創意是不可缺的。但創意的實際化是否需要
一套方式來規範呢?或許僵硬的限制是扼殺創意的的元兇,但缺乏一套記錄、表示創意的
方法,將會讓創意白白的從指縫間流走。或許你看完這篇文章後會說:『我從來都不依照
這些法則來寫我的程式啊!』是的,許多人都是如此,但也因此他們程式的發展都有個
上界,這個上界可能是程式的大小或複雜度等等。當然真實的設計工作不會這麼死板,
你可以自由的運用這些軟體發展的策略,來設計你的程式,要知道,草率的設計和編碼
所導致的結果總是痛苦的除錯!

很明顯的,限於篇幅,筆者不可能將程式設計的方式完全寫出來,讀者有興趣當作
更進一步的研究。不過累積自己的經驗是很重要的,這些實務上的經驗幾乎是很難從書本
上習得,特別是對我們中國人而言,很多設計的理論完全是國外研究出來的,但諸如中文
的處理,卻是完全本土化的一種東西,但國內真正在將技術本土化的人並不多。附帶一提
的,在模組化設計的方法中,有一個由 Stevens 所提出來分別模組緊密性的方法,這個
方式完全就是架構在英文文法上,對中文並不完全適合,因此筆者就捨棄不提了。

對於創造完美的程式,永遠是設計師們追求的目標。你呢?

 


2007年11月16日

物件導向的天空

前 言

「物件導向」這個名詞越來越熱門,似乎不懂它的人都好像活在上古世界一般的過時,但知其然者未必知其所以然,物件的定義為何?物件導向的設計哲學精髓為何?這些問題並非是人人都能答得上來的。

最近學C++的人口也開始上升,恐怕又會帶動另一股流行的風氣。但荒謬的是大部分C++的教科書所講的內容根本就是C語言,外加一點物件導向程式設計(OOP)的皮毛。這樣做真能學好C++嗎?筆者見真正把C的設計哲學搞懂的人士實在不多,恐怕真正弄懂C++的更少。如果缺乏物件設計哲學概念的人用C++所寫的程式恐怕也只是比較特別的C罷了。
其實物件導向是門很有趣的設計哲學,它試圖由人們對問題的看法來解釋軟 體的架構,併力求將問題的定義域映射到分析的模型,再轉成相對的程式碼。目 前使用物件導向技術的軟體如雨後春筍般地蓬勃發展,因此無論你是不是專業的 工程師,都應該對物件有所瞭解。
在後文中,將以物件導向軟體工程論為討論的核心,最後觸及C++與物件設 計的關係。如此精采的文章,你若再不猛K,豈有國法天理人情良心乎!

第 一 章 何謂物件 (OBJECTS)

物件的原文是Object,國內也有人把他譯為「個體」,「項目」、「目的」 等等,但為統一起見,在本文中一律統稱物件。而以物件為基礎的軟體設計哲學 ,則一律冠以「物件導向」 (Object Oriented,簡寫為OO)的頭銜。故物件導向 程式設計便稱OOP (Object Oriented Programming ) 物體導向分析(Object Oriented analysis, OOA),物件導向程式語言(Object Oriented Programming Lagnage,OOPL)如此類推,不一而足。然而,到底物件是什麼呢?電腦學家給它 一個定義:
一個抽像體、概念、或是一個有明確界定範圍的事物,並且在我們要解決的 問題中是有意義的。
舉兩個例子:筆者有部腳踏車,這是個物件,閣下手上看的建青也是個物件 。生活中形形色色的事物也都是物件,閣下亦復如此。這點概念大家都可以接受 ,但光定義物件沒用,車子依舊在路上跑,和閣下的程式毫不相涉。再進一步去 研究,筆者的自行車是以鋁合金為車骨,輕且不生�,而且它可以前進,可以剎 車,這也都沒啥奇怪,但經由以上的描述,我們已經定義出筆者的自行車,和它 的特性(鋁合金車架),及它的功能(前進、剎車)。當然這樣可能尚未臻周延 ,因為我沒說我的車子有輪子,但我的程式或許不必知道那麼多!假定讀者你也 有部腳踏車,你的腳踏車的車身是塑膠製的,同樣的,您的車也可以前進、剎車 !理所當然,你的車也是個物件!上面提到筆者的及閣下的自行車,但其它的張 三李四也會有自行車,其中有鐵的,也可能有紙糊的!此時我們可定義一個叫「 自行車」的「類別」(class)。類別也同樣有個定義:
對於具有類似性質、相同的行為、意義及共同關係的物件之描述即為類別。
淺白的說,類別即是具相同性質物件的集合;反之,對於類別而言,物件則 為其「案例」(instance)。
傳統的軟體設計方式是將問題的對象分析之後,採取適當的資料結構來表示 ,並以相對的程序函數演算法來處理資料,兩者看起來似乎獨立而無關。而現在 我們則以「物件」將相關的資料和程序結合成一體,使得兩者的關係看起來更明 白,這是物件導向程式設計最大的特點之一。
資訊隱藏(information hiding),也就是物件的封裝性(encapsulation), 可以明白分出物件可以被外界使用的特性及受保護的內部特性。把特性依存取權 限分類可以避免程式的其它部分誤用此物件,而導致不良的副作用。關於封裝性 在第六章有更深入的討論。
Cox 氏認為我們可以藉由建立物件程式庫來達成軟體IC的目標,理由是因為 物件具有重覆使用性(reusability),而繼承(inheritance)則是達成重覆使用的策 略。叫它繼承可能有些語意不明,不過卻很貼切,以自行車這個類別來說,加裝 一具馬達就成了一部電動機踏車,電動機踏車依然可以前進、剎車,也有車身材 質,只不過多具馬達,因此這個電動機踏車類別便是由腳踏車遺傳而來的。繼承 在視窗的設計上特別有用。由最基本的視窗可以衍生出可捲頁的,可放大縮小的 ......等等各式各樣的視窗。詳細的內容在後面的章節有更深入的介紹。
火車、汽車、機車都可以為人所駕駛。「駕駛」這個動作是個相當抽像的名 詞,火車、汽車、機車的駕駛方法都不一樣,但駕駛一詞涵蓋了實際上具有差異 性的駕駛行為。這種特質稱為「多形性」(polymophism)。
另方面,光知道物於物件的特性實在不足以解決我們的問題,如何將物件引 入軟體設計中呢?如何分析物件?如何遺傳?如何描述一整個系統中物件和物件 間的關係?這些還都是未知的問題。在後文中,筆者將以相當篇幅討論物件方法 論。在這個領域中應首推布奇(Booch)的研究成果,他有效的以圖表分析出物件 的結構及關聯。不過我並不打算多談,因為他的理論較偏重於傳統的設計理論, 對於遺傳等先進的概念顯得較薄弱,筆者將以勞恩森 (Lorensen) 等人所提出的 OMT ( Object Modeling Technique ) 為骨幹。這套理論有一套極完整的發展過 程。OMT是他們用以發展軟體的方法,因此在實作方面的效果相當理想。
物件導向軟體工程有項特點,就是試圖以建立「模型」的方式來分析問題。 OMT法尤甚,它以三種基礎模型來敘述整個軟體的結構、運作模式,它們分別是 :

*物件模型(Object Model)
描述物件的結構及物件間的關係
*動態模型(Dynamic Model)
系統的狀況時時都有變化,動態模型以事件發生和狀態的改變來描述系統中 的交互作用。
*功能模型(Functional Model)
功能模型是OMT包容傳統軟體設計的一部分。它藉由資料流程圖(Data Flow Diagram,DFD)來描述資料轉換或計算之流程。關於DFD,讀友可參考建青 94期筆者拙作「軟體設計的思維」一文。
OMT的軟體設計步驟是分析(Analysis)、系統設計(System Design)、物件設計(Object Design)在來是實作(Implementation)之本質則是以物件觀點建立前 述三種模型,再加以設計、統合、然後映射成為程式碼。
藉由物件導向的方式來設計軟體可以有效的提升軟體設計的效率,(雖然在由舊方法轉入物件導向時可能會有降低的現象)。舉個明顯的例子:Borland 公 司在Quattro推出半年多後推出Quattro Pro,三個月後視窗版就問世了。這麼快的 速度完全是拜物件導向法所賜,也使Borland公司大賺其錢(該公司號稱年成長 率百分之兩百)。以生產dBASE著名的安信達公司在dBASE III推出數年後才推出 dBASE IV 1.0版,而且還是伴隨一堆Bug上市的。最後,在幾個月前Borland終於 併吞了安信達!

1.2物件模型(Object Model)

物件必定具備兩個基本的構要素:屬性(attributes)及行為(behavior),也就是物件的運作(operation)。再舉前面的例子,自行車類別中關於車身材料等 等靜態的資料就是屬性,而自行車可以做的動作,如前進、煞車等等都屬於屬性 。也就是說,類別中靜態的資料成員就是該類別的屬性,而該類別動態的演算法 就是這個類別的行為或是運作(operation)。那何謂運作,何謂方法(Method)呢 ?兩者有什麼不同呢?前一節曾經提到「駕駛」這個抽像化的工作,「駕駛」在 此便是個operation,而實際的、真正的駕駛則是屬於各別物件的特定方法 (method)。例如說,開轎車的方法和開七四七就不一樣,但一樣都都被稱為是駕 駛,但這不是掛羊頭賣狗肉的勾當!
請看官大爺注意,在還未實作之前,所有的概念都是抽像化的,因此腦中除了考慮你的問題外,不必急著去想在電腦上設計的細節,除非你所要解決的問題 本身就與電腦有關。

1.2物件之間的關係

1.2.1鏈結(Links)

假定我們現在有二個物件案例(instance),一位是張老師,一位是王同學。張老師和王同學這兩個物件的屬性和行為我們暫且不關心。張老師與王同學之間 存在某種關係,由參考圖1.1可以看出張老師「教導」王同學。反過來看,王同 學則「受教」於張老師。這兩個案例間的關係便是鏈結。

1.2.2 關聯(Associations)

在Link鏈結的部分,我們的案例(instance)是張老師和王同學,但老師可能是趙錢孫李,同學也可能是周吳鄭王。將兩個案例一般化後,我們得到兩個單純 抽像的類別;老師類別(class teacher)及同學類別(class student)。各同學各 老師之間鏈結的集合便稱為關聯(associations)。也就是說,鏈結是真實世界中 物件的關係,而是將鏈結抽像化,一般化後我們便可得到關聯(association)。
讀者在此應先建立一個概念,物件導向的分析規則是由特殊到一般,由實體到抽像,就像我們先以張老師和王同學為研究樣本,再將他們的關係抽像化。往 後還有更多的例子。
一個老師所施教的對象通常有許多位學生而不只是一位。此時,這個教導的關係便成一對多(one-to-many)的關係,如圖1.2所示。而若一個學生只向一位老 師求教,教導關係則為1對1(one-to-one);若一個學生不僅僅只有一位老師,教 導關係就成了多對一了!請讀者試著看懂我們所用的圖形表示法,並且比較其間 的差異。在真實世界中,關係是相當具多樣性的。例如說它可能是個選擇性關係 (optional association)。例如在一夫一妻制國家中,一個男人可以選擇單身( 無妻)或有唯一的配偶,如圖1.3所示。但中國古時候是行一夫多妻制的,窮漢 子可能一個老婆都沒有;市井小民或許只養得起一口子;天子王侯可能就藏嬌無 數了!這就如圖1.4所示了。圖1.3中的關係是1或無,圖1.4是無或許多。筆者在 此特別聲明我絕對沒別的意思。
關係(association)也可以不只限定於二元關係(binary association),它可能是三元性(ternary association)或多元性的。圖1.5 表示老師、學生及教 室的三元關係。

1.2.3 鏈結的屬性(Link attributes)

前面所提到的鏈結都蠻單純的,但真實情況中,鏈結可能有條件或範圍上的限制。例如在一個多使用者(multi-users)的作業系統(operating system)中, 每個user對檔案的存取都有不同的權限,這點在93期建青筆者拙作「切開MS-DOS 」中有介紹,各位看官可翻閱參考一下。使用者依其層級不同,可以讀寫檔案, 或僅能讀檔,甚至連沾邊的機會都談不上。這麼做的理由是為了保證檔案的安全 所設的規定。此時User這個物件在對於檔案物件的存取關係中,便具有存取權的 屬性。如圖1.6所示。
一個較特殊的鏈接屬性稱為資格(qualificiation)。可將其視為是具有邏輯性的屬性。例如關卡的通行許可。「通行」只有准許及拒絕的判定,不可能只允 許前腳進,後腳出,身體跨在門中間。因此這種關係是「資格」的判定問題。

1.2.4 集合(aggregation)

「集合」表明了一種物件組成的關係。例如汽車物件是由輪子物件、引擎物件、齒輪箱物件,車殼物件等等物件所組成的集合體。如果A物件是B元件的一 部分,B物件是C物件的一部分,則B物件亦會是C物件的一部分。則A物件是 C物件的一部分。A物件是B物件的一部分。則B物件必非A物件之一部分!
這個概念在視窗設計上也很有用!你可以利用按鈕物件和視窗物件組合成一個對話盒物件。集合關係(aggregation)很容易就和遺傳或其他特性搞混了,讀者 宜注意。

1.2.5 遺傳(Inheritance)及歸納關係(Generalization)

我相信只要是對物件導向稍有接觸的朋友都必定對「遺傳」(inheritance) 這名詞不陌生。遺傳和歸納關係提供了程式碼重複使用的可行方案。由生物學的 觀點來看遺傳:子代的特徵都來自於親代。由電腦的觀點來看遺傳:遺傳就是類 似軟體元件共用,並且分享相同的行為以及相似的特性或運作。一個被遺傳的父 代類別 ( parent class ) 被我們稱為「超級類別」(super class)。而子代類 別(child class)被稱為副類別(subclass)。父類別和子類別之間則存在有歸納 關係(Generalization),而同時也是表示」is-a」或」a-kind-of」的關係,這 代表什麼呢?子類別的案例(instance)同時也必須是父類別的案例。這項特性十 分的合理,假定子類別被修改的和父類別不同,悲劇將無可避免的產生:適用於 父類別的運作(operation)無法正確的適用於子類別上,而設計者在不知情的狀 況下,軟體可能會當掉或失誤。但子類別並非不能修改。你可以修改(refine)及 增加(append)一個子類別的屬性(attribute)及運作(operation)之內部方法 ( method ) ,請讀者回憶前面章節曾說明過運作和方法的差異。運作(operation) 是一個動作的「簽名」(signature),也就是某一動作的名稱。但筆者在此所說 的修改,是屬於「機能性」的修改,意謂為了適應新物件的運作方式,而必需修 改其內部的方法(method),以符合新的要求,但該方法最後所要達成的目的應該 和舊方法是一致的,非重新定義。例如我們把」+」定義為運算上的加法,而你 卻要定義為減法,這不是挺危險的嗎?不幸地,目前的程式語言並不會主動地警 告設計者這個潛在的危險,而成為一個軟體陷阱。
剛才我們曾討論了一個重點:為了適應新物件的新特性,而必需修改其內部的方法,但運作的簽名(signature)卻必需維持相同,這種特稱性便為「凌越」 (overriding),但和C++中的「多重載入」(overloading)並不相同,請讀者自行 比較。
在此舉一個範例。在一個幾何繪圖的程式中,我們嘗試提供點、線、弧線、多邊形、圓形的繪圖元件。在此範例中,筆者將以物件分析(OOA)中關於物件模 型的方法來分析這個例子。
我們先試著描述這個繪圖系統的各種圖形元件(先分析案例)。因為用文字說明佔去篇幅繁浩,故把各物作類別的特性列於圖中。決定各類別後,得視情況 將類別多餘的,無效的屬性或運作剔除,並且將一些可以做成物件的特徵獨立成 為物件(不過在範例中似乎沒有)。下一步是鑑別物件與物件的關係(link),單 就這些物件來看也缺乏關聯。接下來,觀察這些物件是否用了一些共同的特徵( 屬性及行為),將共用的特徵萃取出來,形成父類別。在範例中,我們可以先將 物件分成三類,分別是零維、一維及二維圖形(用generalization),而且一維及 二維圖形可以按比例(scale)伸縮,但三維圖形中間空白部分則可以填色(fill) 還可以把顏色、位置、移動....等等特徵歸納成一個最終的父類別,也就是 圖形(figure)類別。如此我們又得到一張新的參考圖1.7。同時,在此也可以考 慮引用舊的物件。
在圖中,我們發現物件顯示(display)的功能被凌越(overriding)了!為什麼呢?因為畫法不同啊!但圖形(figure)那個類別要幹啥呢?一點用也沒有!但 他可以表示各subclass的共同特性。任何類別隻要有一個運作是抽像的 (abstract),這個類別就是一個抽像類別(abstract class)。抽像類別不能產生 案例(instance),但卻可供為遺傳的父類別。相對於抽像類別的是實體類別,實 體類別可以衍生子代,也可以產生案例(instance)。
另一個爭議是應該遺傳幾代。遺傳太多代可能會使子代不好懂。太少嘛,不太合乎經濟效益,二、三層很好,六、七層勉強接受、十幾層恐怕令人咋舌了。 為什麼呢?是這樣的,理論上類別應該可以被無窮的遺傳,產生新的類別,但是 常常發生在遺傳數代後的子類別的特性竟然「變質」了!父代類別的舊屬性或是 舊運作變得囉唆多餘了,如果你繼續引用遺傳,那可能會變得沒有效率,因此, 筆者認為物件重複遺傳的次數應該會有一個自然上限,但這個上限是幾層呢?這 就和你所設計的物件好壞有關了。

1.2.6 多重繼承(Multiple Inheritance)

前面所提到的都是單源繼承(single inheritance),十分單純,相較之下,多重繼承允許一個物件同時繼承許多不同物件的特性,這種方式更接近人類思考 的方法,不過相對也產生不少問題。所謂多重繼承即是一個子物件可同時繼承許 多父類別,通常我們叫這種子類別為「聯合類別」(join class)。
多重繼承最常引發的麻煩便是「模稜兩可」(ambiguity) 的紛爭。這肇因於父類別中的屬性或運作可能具有類似的簽名。這種情況無法以程式語言的特性避 免(雖然某些程式語言可以設定繼承的優先順序),程式設計師應該要盡力避免 模稜兩可的問題。另外值得注意的一點是某些較舊的物件導向程式語言(OOPL)並 不具備多重繼承的特性,例如:Smalltalk-80,C++ 1.2,Objective-C。

1.3 集合與其它關係的比較

物件與物件之間的關係是極令人感興趣的,一個程式不太可能僅由一些簡單的物件架構而成。前面筆者曾說過集合(aggregation)很容易和其他種類的關係 搞混,在這節中,我們將討論它們有啥差異。讀者在往下看之前,不妨自己先比 較一下,看看自己是否也搞混了。

1.3.1 集合和關係(association)的比較

集合是關係(association)一種特例,但集合關係卻特別指出某個特定物件是由那些物件組成的。就好比一部機器與組成它的螺絲釘之間的關係。主要的判 定方式是:如果一個物件是另一個物件的組成元素,那麼便存在「集合」的關係 了。

1.3.2 集合和遺傳(Inheritance)的比較

集合和遺傳並不一樣。遺傳關係通常被稱為"is-a"或"is-kind-of"(是一個,是一種),理由前面已經講過了,而集合則被叫做"a-part-of"(一部 分)。
不過,有時我們最好選擇集合關係來替代遺傳的運用。先舉一例。串列結構 (list)是資料結構中一種儲存資料的基本方法,而堆疊(stack)是一種 FILO ( first in ,last out ) 先進後出的結構,關於這方面請讀者自行參考與資料結 構有關的書籍。串列有些基本的運作方法,例如說:增加節點,刪除節點等等。 現在我們要實作堆疊,而堆疊有自己惟一的存取方法也就是它的運作──推入 (push)和彈出(pop)。我們可以採取兩套策略來獲得「堆疊」類別。第一:由串 列遺傳出新的「堆疊」類別,但注意!那些刪除節點之類的運作(operation)也 會被遺傳下來,然而對堆疊來說,不但不需要這些多餘的運作,也不能要,因為 可能意外地刪除節點。因此建議採第二種策略:把串列和堆疊以集合 (aggregation)連起來,就是說把串列變成堆疊的一部分。如圖1.8所示。
關於物件模型(object model)的部分就介紹到此,由於物件模型是最基本而重要的一部分,讀者宜多留心。

第二章動態模型 (Dynamic Model)

2.1 概說

上一章所討論的物件模型,是屬於物件導向中靜態的部分。而這章則要仔細地探討物件的行為,及物件之間的交互作用。

2.2 事件(events)和狀態(states)

概括的說,物件中的屬性及鏈結(link),都屬於該物件的狀態(state)。而 一物件之狀態則會因事件(events)的影響而改變。在下面的討論中,讀者會發現 我們較偏重於即時(real-time)程式的設計,事實上亦是如此。
事件是指發生在某時刻的某事,而且理論上它並不會持續一段時間。例如電話鈴響就是一種事件。事件通常由一個物件發出,而被另一個物件接受。而且事 件只是單向性的訊息傳遞,而不像函數會帶有傳回值。事件也是一種特殊的類別 ,他可以具備有屬性。例如說:撥電話號碼(dial)這個動作也隱含著撥某個號碼 ,例如說:撥」5」、撥」1」等等。同理,「錯誤」也可以被設計成事件,而且 伴隨有錯誤代碼。
狀態(states)是物件屬性或鏈結的抽像體。狀態是被動的而且只在此事件發生後才改變,因此我們可視為兩事件發生間的時間為此物件的狀態,也就是說, 某個狀態將會持續一段時間,直到下個事件發生為止。
為了表示出一連串的事件及他們發生的順序,我們得編一套事件的「劇本」 (scenario)。這個劇本十分重要,有劇本才能讓模型」活起來」。劇本的產生通 常是以平常可能發生的情況為主體,再加入例外。然而,劇本也有可能不甚詳盡 ,但是可以改!
劇本準備好了之後,可以作事件追蹤(event trace)的模擬。事件追蹤可以明確的看出來誰產生了事件,誰接收了事件,何時產生事件,那個事件先發生... ...等等。接著再作一份事件流程圖(event flow diagram),顯示哪個物件該產生 那些事件,該接收哪些事件等等。
狀態圖(state diagram)則很清楚的表示了事件的流程及對狀態的影響,以下將教您如何畫圖。在狀態圖中,用節點(一個橢圓形)表示此物件的狀態 (state),而箭頭旁則標明了事件名稱,在事件的流程圖上可加以標示「判斷」 布林值以決定事件的流向,在後面有張紅綠燈的狀態圖,在此扼要的加以說明。 這個號誌系統是個長「眼睛」的紅綠燈,藉由偵測在左彎車專用道上是否有車, 來決定是否要讓直行車先行或留一段時間給轉彎車走。你可以看得出來,例如說 南北向要彎紅燈時,會先判斷要不要先亮轉彎燈,如果不要,就直接跳到東西亮 線燈的狀態去。在此,那個判斷值便標明在事件的箭頭上,其餘依此類推。

2.3 動作(Action)與活動(Activity)

光在圖上標出事件、狀態及判定值還不太夠,通常伴隨事件之後應該會有一項對應的動作 ( Action ) ,它在圖中的表示法是在事件敘述後加斜線」/」區 隔。事件的發生只是告知或確認狀態的改變,至於如何轉變狀態則和事件無關, 必須靠內部的活動 ( activity ) 來改變狀態。而這活動可能包括一連串的運作 ( operation ) 。活動 ( activity ) 在圖中的表示方法是在節點中寫個 do:activity name。讀者或許感到筆者一直在強調狀態圖(state diagram)的畫 法。因為構圖表示一個程式十分的重要。前一章物件模型有圖,這章狀態模型也 有圖,下一章功能模型還有DFD圖。由此可知圖表的重要性!讀者應該好好的 揣摩圖形的畫法。

第三章 功能模型(Functional Model)

功能模型 ( Functional Model )是OMT 三種基本模型中延用「傳統」的一部分。功能模型用以描述系統中資料的流程,資料被哪些單元接受處理經由哪些 過程,但卻不深究資料真正是怎麼被計算的。功能模型有效的運用在編譯器 ( compilers ) 之類純粹處理資料的工作,而這些工作所處理的資料不易於被設計 成物件。不過在傳統的設計領域中,除去物件模型,程式一樣可以寫,只不過在 這裡以物件分析為首要觀點,功能模組只得屈身。與其它兩種模型並列了。

3.1 DFD資料流程圖

資料流程圖的繪製是功能模型表示方法的核心。DFD圖的畫法較前兩章圖形的畫法都來得簡單,以下就各圖形元件來介紹。

3.1.1 處理單元(process)

我們以一個橢圓形的節點來表示一個「處理」。「處理」(process)用以接受輸入資料,經過一特定工作方法,將輸入的資料轉換計算後輸出。

3.1.2 資料流(Data Flow)

資料流表示出處理與處理之間所傳遞的資料,在此我們以箭頭符號連接。在某些例子中,一道資料流(我的意思是僅以一個箭頭符號來表示一道資料流)可 能包含許多資料,而我們或許需要分解其中各別的資料流,此時可以將它分流成 更細的資料流,流入目的「處理」單元。

3.1.3 控制流(control flow)

資料流(Data flow)只是單純的傳遞資料,而不傳送控制訊息。然而某些處理(process)會需要控制旗標來決定它資料處理的方式,解決的方式並不是經由 資料流傳一個值給處理,在此我們我們另以控制流來解決這個問題。控制的性質 是布林(boolean)代數值,只有真偽兩種情況。基本上控制流應該屬於動態模型 (dynamic model)中的」事件」,但因為和資料處理有密不可分的關係,於是便 被包括進來了。控制流的畫法是以點虛線箭頭表示事件訊息。

3.1.4 資料儲存(data storage)單元

資料儲存單元被視為DFD圖中一個專門用以儲存資料的被動物件。資料儲存單元的畫法是以二槓橫線表示它。

3.1.5 行事者(Actor)

Actor被用來標示資料流的端點,可能是起點,也可能是終點。畫法是以小三角形來表示。Actor和資料儲存單元有幾分類似。但資料儲存單元是被動的, 而不會執行任何動作,但Actor卻依資料的指示作一些動作,例如產生資料或終 結資料的處理,因此有時也被稱為「終結者」(terminator)。

3.1.6 外界個體

我們用一個矩形來代表外界個體。外界個體是什麼意思呢?因為在DFD圖中總會有資料是由外界輸入進來,也會有資料傳送給外界。因此以外界個體來表 示系統輸入的起點及輸出終點。

3.2 物件、動態模型與功能模型的關係

功能模型描述了一個軟體模型真實地去作了哪些事,DFD圖中的「處理」(Process)單元可以對映到物件模型中的運作(operation),而資料流則是物件內 的屬性。控制流(control flow)前面也說過是來自於動態模型。整體的來說,三 種模型並不是獨立存在的,而是互補的,只是說它們各自以不同的角度來分析軟 體。功能模型中資料的流程及處理的流程圖並不能表示出處理執行的順序,這點 必需靠動態模型來補足(還記得原因嗎?)。而處理與處理間的流程關係及對於 物件運作(operation)的主客關係。這裡主客關係的意思是:運作(operation)之 間的層次關係,主模組將它的任務切割交由底下的子模組處理。物件模型內的屬 性,也就是狀態的改變,以及運作是如何被引發,怎麼個執行法,都由動態模型 來表達。因此都於所有的軟體來說,三種模型都是基本而且必要的,只不過是重 要程度不同而已。稍後我們所討論的物件發展流程也是照著這三種模型來發展。

第四章 分析 (Analysis)

4.1 OMT的設計流程

OMT方法其實就是一種軟體工程發展法。在此體系中我們採取下列步驟來設計軟體:

*分析(analysis)

分析工作由瞭解問題定義開始,逐次建立物件、動態、及功能模型來描述軟體定義域(software domain),這件工作和真實世界有極大的關聯。

*系統設計(System Design)

以物件模型(Object Model)為基礎,開始建立系統的架構,定義動態模型的劇本,將分析真實世界的結果轉換成電腦世界中型態。

*物件設計(Object Design)

如果說系統設計是承先啟後的的工作,那麼物件設計就是集其大成了。在本步驟中,三種模型被我們仔細的修改,細部化,具體化,以至於最佳化,將 模型轉換成實際可行的方案,然後進行模組化,最後將結果交給下一步處理 。

*實作(implementation)

實作就是將前面所得的結果轉成對映的程式碼。往後在此章節中,筆者將和大家討論C++的應用。

4.2 分析

這部份的內容在前三章的內容及概念己經被打散進入各模型的介紹中了。我們現在的工作就是把他們整合起來,並加強一些重點。
分析工作就是要瞭解問題的定義,因此第一步,整理關於軟體的描述,弄清楚問題的細節,力求使定義明確而易瞭解,也就是說要由問題陳述(problem statement)來分析軟體的結構。這工作的基本精神和傳統的設計方式是殊無二致 的。筆者曾在前期的拙作中解釋得很明白了。
物件模型的建構:首先把一個物件的結構包括屬性和運作行為列舉出來,找出物件與物件間的關係。並且要將物件中和問題無關的屬性及運作刪除,同時也 要去除和問題的屬性及運作刪除,同時也要去除和問題無關的關係及鍵結。適當 的引用舊物件,或將同類的物件抽像化,取出共同的父類別來....。有一點需要 留心,最好要有說明文件來記錄物件的性質和運作的目的。相關的內容在第一章 中都說過了,請讀者自行參考。
動態模型表達出物件中事件運作的順序和狀態的改變。首先,你該準備一個劇本,劇本的來源或許是積數十年的觀案所累積的規則。劇本的內容或許不是鉅 細靡遺,也可能和「連噓劇」一樣無聊,但它卻很重要。然後要找出那些是「事 件」,事件包括信號,輸入,中斷等等。同時要留心每個伴隨事件而生的動作 (action),及其副作用。然後以劇本為根據作事件追蹤(event trace),測試事 件發生的先後順序及各事件的目標物件(target object)。
使用者介面 ( User's interface ) 也該在此時期建立。使面者介面的設計是最難估量的,因為你很難決定怎樣的介面才是最好用 ( easy use ) 和最友善 ( user friendly ) 的,除非到真正設計完成後才能知道效果如何。然而,我們 不得不承認軟體是由程式邏輯的部份和使用者介面合起來的。尤其在今天,軟體 的好壞通常經由使用者介面判定。使用者介面的設計在目前而言並沒有一個很統 一的型式,但將來也很難有一致公認的標準。

關於分析功能模型 ( Functional model ) 方面,主要的工作就是建構DFD圖。將資料的流向和處理程序標示出來,就如同前一章所講的一樣,然後本著 「逐步細緻化」的原則,將抽像的處理單元,再細分出它內部的DFD圖,一真 重覆這個步驟,直到它們的結構基本到不能再基本為止。

第五章 系統設計(System Design)

系統設計是軟體設計週期中承先啟後的一環。在本階段中有一些首要的目標,包含模組化、資源分配、系統結構規劃等等工作。

5.1 模組化

再一次要強調「逐步細緻化」的概念。概念上是將一個大系統切成小系統, 小系統再分成小小系統.....如此遞迴式的切分,可以分析出基本單位、指令碼 ,所以一軟體可能是由幾個主系統,而主系統又由若干個小系統所組成的。我們 試著歸納主系統及子系統間的關係,有層疊(layer)及分割(partition)。

5.1.1 層疊(Layer)

具有層疊關係的從屬模組,其現象是;子模組提供父模組所需的功能,子模組並不需知道父模組在做什麼,更不會呼叫上層模組。因此,層疊的構成是「垂 直」的相繫。層疊式的關係可以降低機器相依性(machine dependence),因為只 有最底層模組才需「實體化」,中層以上的模組所作的工作都可以是抽像的。因 此為了要適應新的環境,只要抽換底層的模組就可以了。

5.1.2 分割(partition)

分割表示說,模組和模組的連繫是水平方向的。模組間可以相互的呼叫。分 割及層疊結構通常都共存在系統中間可以相互的呼叫。分割及層疊結構通常都共 存在系統中,也沒有孰好孰壞的問題,依系統所需才重要。

5.2 管理資源(resource)

磁碟、螢幕顯示、記憶體、鍵盤、印表機等都是系統資源。管理系統資料的 工作大半都有作業系統把關。不過應用程式也不應任意讓程式內的次系統隨意使 用公共資源。最好的作法是這樣:設計對等於資源的把關者物件,使該物件代表 該資源,由此物件統籌此資源的管理。例如說你可以把某個檔案定義成一個物件 ,定義一些運作.....。當然可能會對存取速度或效率有點影響,但那也是極少 的一點而己,但卻可以用一致的方式來管理資源。

第六章物件設計(Object Design)

物件設計(Object Design)的工作是規劃工作的最後一環,在這個部份裡, 我們得把模型具體化,最佳化,才能實作。

6.1 演算法(Algorithms)設計

演算法的重要性是無可比擬的。一個好的演算法不但快,而且有效率。請看 這個方程式 f(x)=10*x^250-20*X^249+1求 f(2)=? 嗯,很簡單,把2代進去算, 然後一定會發生悲劇,因為250次方實在太大了,沒法算。可是寫成f(x) = (10x-20)*x^249+1 不就很好算了嗎?這是一個數學演算法的例子。如何去選擇 一個適當的演算法呢?嚴格說來這點將牽涉到演算法分析,需要引用大量的數學 估算,恐怕得寫上一本很厚的書。不過就軟體工程的觀點來看就比較簡單而直觀 了。請各位把握一些要點:選擇那些公認而且沒有爭論的演算法,它們通常是有 效的。但如果你自己設計演算法,將一般性列入考慮。如果演算法不是要被用在 關鍵瓶頸上的,最好是易懂易改的。

為了實作演算法,也同時得選擇合適的資料結構。既然有靜態的資料結構和 動態的演算法,我們可以試著做一些內部類別(internal class)來應付實作演算 法。現在大部份的物件導向程式語言都附有資料結構的物件,可以減去重新設計 演算法的麻煩,真是設計師的福音。

6.2 關聯(association)的設計

我們好像很久都沒看到這個偉大的名詞了。筆者曾提及關聯的運用是很重要 的,但怎麼實作關聯呢?我們試著以關聯用途的方向來分析。

6.2.1 單向關聯(One Way Association)

好幾章前筆者曾以老師及同學類別的交互關係來說明關聯。如果單就「教導 」這關聯來看,純粹是單向性的由老師到學生,而沒有學生「教導」老師。單向 關聯的實作通常以指標達成。如果關聯是一對一(one-to-one)的,則用一個指標 就可以解決,但如果是一對多(one-to-mamy),則可能要用一組指標,或一個指 標的表格。但如果這關係是有條件限制,則一定要用一個帶有權限值 (qualificiation)的表格來連接了。

6.2.2雙向關聯(Two-way association)

在一些情況下,雙向關聯是有必要的。如果去的方向使用率大於反向的使用 率,那麼和單向關聯一樣,用一個指標聯起來就足夠了,在需要用反向關係時, 以搜尋的方法來找到正向指標的來源物件。如果來去雙向使用頻率都差不多,那 麼便用雙指標。在複雜的情況中,例如具有條件的關係或是多對多的關聯之類的 運用便有須要將關聯做成一個物件。基本上,這物件也是一個表格。關於這些類 型的表格,可考慮以雜湊表(hash table)來實作。

6.2.3關聯的最佳化(Optimization)

我們「物件模型」中所建的物件及其關聯可能是很完整而週密,而且符合現 實的。不過它可能太繁複了。為了傳送一個事件,可能要穿過數個物件,透過數 個關係(association)傳遞。就好像從台北到高雄,走三號省道,得經過近百個 城鎮和數不清的支線才能到達目的地,但如果經過高速公路,就可以直達高雄。 同樣的,在資料傳送頻率高的物件間,即使彼此沒有直接關聯,也可以考慮特別 建立一個關係(association)來達成最佳化資料傳送路徑的目的,以去除傳輸過程 所不需要的冗長步驟。

6.3遺傳(Inheritance)的運用

物件導向的特點就是它具有遺傳性,我們希望軟體中的物件能夠儘可能地重 覆使用,繼承舊物件,以大量縮減軟體設計的時間及增高可信度。

物件中的一些運作(operation)可能和舊的物件或他同系統中類似的物件中 的運作有相近的功能,但其參數個數卻少了一些。就因為這樣,可能會導致無法 遺傳舊物件,這實在的很可惜。不過,何不替這種運作「補足」多餘的參數呢? 這真是一個好主意。藉由增加多餘的參數,它們的簽名和功能就可完全一致了! 例如說在單色繪圖系統上畫點,只需要座標的資料即可,但在彩色系統上卻得要 有顏色(color)的參數,如何達成共用呢?只好在單色畫點的函數中多加一個不 需要、可忽略的顏色參數。讓兩個原來並不相同的運作(operation),變成簽名 (signature)相同,但內部方法(method)不同的同一個運作。

除了遺傳舊物件以外,新建的類別也可以想法子做「遺傳」。因為相類似的 類別都具有共同的運作及屬性。考慮將這些共同的運作和屬性抽出來成為一個共 同的超級類別。但基於各子類別具有差異性,則某些運作可能是抽像的,於是乎 這個父類別就成了抽像類別了。

這樣作有不少好處。第一:修改其共同的父類別就可以影響到所有的子物件 。第二:可以產生不同的子類別來適應不同的環境。對於前者,筆者認為很淺顯 ,不需多討論,讀者應該可以瞭解;筆者要歌頌一下第二點。假如你的軟體公司 出售同一套軟體給許多有不同需求的客戶,你毋需大幅增刪你的程式,只要遺傳 新的子類別即可。其次,以一個繪圖系統的繪圖類別而言,為了適應不同的顯示 系統,可以遺傳適應不同機器的類別,在真正上機執行時,再依真實情況引用適 當的類別就可以了。

6.4資訊隱藏(lnformation Hiding)

在前面章節中,幾乎沒提到資訊隱藏(lnformation Hiding),也就是物件的 封裝性(encapsulation)。封裝性和資料保全(security)不能混為一談,後者指 的是系統資料的保密和安全的可靠性,前者則具有軟體工程上的意義。資訊隱藏 的目的有二,第一:使運作(operation)有限度的存取資料成員,降低其引發的 副作用。也就是說,我們不希望運作會牽一髮而動全身,因此在修改運作的方法 (method)時,可預期的降低物件結構的影響。第二:迫使物件必須建立一個統一 的介面(interface)。透過介面來提取或改變物件屬性,可以有效地遏止類別遭 到意外的破壞。更重要的是,類別介面可以提高類別的抽像化程度,一個愈抽像 的類別對我們愈有利。要怎麼去決定類別中哪些屬性和行為該被隱藏呢?說實話 ,筆者也考慮了很久。一個常見的看法是這樣的:僅將類別的界面顯露出來,而 將其它的元件都隱藏起來。而這又引發了一個問題:我們如何去設計一個好的介 面?筆者心目中好的介面是高度抽像的,而且與內部結構不相涉,能夠考慮到遺 傳後的子類別仍可適用父類別的介面,並且不需要任何增刪修改,但同時也必須 能周延地控制物件的動作。

6.5類別模組化

傳統的軟體設計很重視模組化,物件導向軟體工程同樣也十分重視模組化, 模組和類別的意義並不相同。筆者認為模組的界定屬於「功能模型」的範疇中, 也就是說將有主客關係或是類似的運作模組化。但如何形成模組呢?模組間又存 在什麼關係呢?我們依舊以「緊密性」來判定模組內成員的關係,而以「關聯性 」來討論模組與模組之間的交互作用。這兩個性質一樣的可以被引用到類別的分 析。模組內若是緊密性低,可能意味著這模組還可以再被切分成更小的子模組。 同樣的,類別內若是緊密度低,那麼這個類別或許也是大而籠統的類別。模組之 間若關聯性過高,應該考慮模組的分割分式是否妥當。同樣的道理引用到類別的 分析上亦是如此。關於模組的緊密性及關聯性的詳細內容,請參考上期建青。

第七章實作、語言

7.1物件導向程式語言(Object Oriented Programming Language)

不可否認的,物件導向設計(OOD)和物件導向程式語言的相依程度極高,物件導向設計的概念在七○年代就出現了,但卻是在八○年代物件導向程式語言發 展成熟之後才大放異彩。傳統的程式語言並非不能應用物件導向方法,只是很難 罷了,例如遺傳 (inheritance) 就很難實作,因此得依賴物件導向專用的程式 語言。但物件導向程式語言並非一夕所成的。從程式語言的發展歷史中,可以看 出一條脈絡來。如果按照一般對語言的分類方式,則有第一代到第四代之不同。 但筆者在此採取資料型態的觀點來看程式語言的演進,歸納出一些結果。上古的 程式語言,如組合語言,沒有今日整數、浮點數的資料表示法;中古的程式語言 如 FORTRAN、BASIC 等有基本的資料型態,如整數、字串型態等等;近代的程式 語言如C或Pascal,則具備有「集合」式的資料表示法,而新一代的程式語言如 Smalltalk 及C++則具有類別封裝的表示方式。

一個理想的物件導向程式語言(OOPL)能夠確實的將問題的定義域(Domain)映射 到程式中。換句話說,在前面數章所描述的各種物件特性都應該被包含進OOPL。 很不幸地,各種語言都不能很完備地包含那些特性。例如說:多重繼承 (multiple inheritance)並不存在於較舊的OOPL中,例如C++ 1.2等。而對於多重 繼承(multiple inheritance)解決模稜兩可 (ambiguity) 的方法也不盡相同。 此外,我們希望程式語言能夠提供一套標準的類別程式庫(Class library),就 像Smalltalk一樣,設計師可以自由運用上百個已定義的物件。而C++,頂多以 iostream的I/O物件充數。各家物件導向程式語言對於物件導向的詮釋都有所不 同,導致於它們在物件導向理論中的細節看法都不一樣,這點讀者不可不注意。

此外目前OOPL產生新類別的方法都是在程式寫作時在程式碼中載明遺傳方式 。將來則可能會出現在執行時期(run-time)可產生物件的程式語言。屆時相當多的問題也可一併解決。 第八章關於C++

雖然Smalltalk的歷史、名氣可能都來得比C++大,但C++的適用性較強。對 於C++的介紹真是多如過江之鯽,諸有興趣的讀者自行參考,在本章中將探討C++ 在物件設計中的應用。

8.1「類別」Vs「類別」

不要以為這節的名稱搞錯了。C++中的類別與物件模型 ( object model )中 的類別有啥差異呢?就抽像化的層次而言,兩者的意義是相等的。就實際情況來 說,C++的類別的成員存取屬性可分為公共(public)、保護(protected)及私有 (private)三區。在不討論遺傳的情形下,保護(protected)及私有(private)屬 均性是類別中被隱藏的部分。而只用於遺傳時,public及protected方能遺傳給 子類別。為什麼要這樣定義呢?這是個很有趣的問題。根據筆者的研究, private成員事實上一樣也會被遺傳給子類別,但不能被子類別的成員存取。這 屬於一種隱性的遺傳(implicit inheritance ) ,但從另一個角度來看,private 成員可以被當成該類別暫時性成員,可能是為了要完成某個運作(operation)內 的方法(method),而需要的內部類別(internal class)或輔助的子模組。這些成 員的遺傳不具意義,也就成了隱性遺傳。

8.2 凌越(overriding)與多重載入(overloading)

凌越及多重載入的性質恰恰相反。凌越在前面的章節就談過了,意思是子類 別的運作取代父類別運作內部的方法,而更換成適合子類別的新方法。而多重載 入(overloading)又被稱為「名稱的重覆使用」。產生的情況是這樣的:函數ABC 被多重載入成兩個函數ABC(int)及ABC(char),也就是說名稱是相同的,但其參 數不一樣,導致簽名(signature)不同。在呼叫ABC函數時,若呼叫的型式是 ABC('A'),參數是字元A,就和ABC(char)的簽名相同,此時便適用ABC(char)這 個函數。不只是函數,連運算符號也可多重載入。複數的運算就是一個明顯的例 子,實數的加法是被加數和加數相加,而複數相加卻是實部加實部,虛部加虛部 ,但卻可以共同使用加號為代表不同的加法運算。

8.3 虛擬函數(virtual functions)與抽像類別(abstract class)

關於抽像類別(abstract class)在「物件設計」一節中,曾經提及它的性質 ,在這節中將看看C++是如何表示一個抽像類別。

虛擬函數的存在為的是要表明該函數的執行內容現在無法定義,或現在已定 義,將來卻可能會被改變。在物件模型一章中,曾提過一個幾何繪圖的例子。所 有的圖形,如圖、線....等基本圖樣都是由一個最原始的父類別,也就是「 圖形」類別。在「圖」類別中有項運作(operation)叫「畫圖(draw)」這項目。 但各個圖樣的畫法卻有差距,因此「畫圖」這項運作在「圖形」類別中存在,但 卻沒法子真正的動起來,只有在遺傳子代,也就是那些圖樣類別中才能定義, 因此這個「畫圖」函數就是個虛擬函數。另外一種情形是這樣的:假定有個「四 邊形」類別,定義它「繪圖(draw)」這項運作當然沒問題。現在假設遺傳出「正 方形」這個類別,我們可以重新定義「繪圖」這項運作,因此這個繪圖函數又是 一種虛擬函數。針對於前者的情形,必須把該函數設為「純函數」(pure function),寫法是virtual ABC (void)=0;而具有至少有一個這算型純函數的 類別便是抽像類別。但後者那種情況的類別便不算是抽像類別,因為抽像類別無 法產生案例(instance),只能用來遺傳出子類別。

8.4 模板(Template)

模板(template)是AT&ampT C++ 2.1新定義的功能。它和物件導向的理論倒是沒 多大關係,不過在減少重複編碼上卻省去設計師不少力氣。

請看這個例子:堆疊(Stack)是一種先進後出(FILO)的資料結構。理想中的 堆疊沒有容量限制,另外,它不限定被推入堆疊的資料之型態,是整數,浮點數 或其它。但在實際狀況下又是一回事。在2.0以前的C++中,你可以多重載入push (推入)這個運作,但寫起來很囉嗦,因為推入的方式都一樣,但只因參數型態 不同,就要重覆寫很多極類似的多重載入函數。而現在用template,就可以由編 譯器自動產生類似的函數,如表8.1所示。因此,事實上也如此,編譯器只是把 這個模板命令視為一個展開巨集(MACRO)的指令而已。

8.5 預設參數值

預設參數值是C++ 新增加的功能。前面曾提過單色與彩色繪圖系統中畫點運 作的一致化時所作的改變。例如說:point(int x, int y, int color=1)這個繪 圖函數的簽名。假如程式拿呼叫單色繪點的方法來呼叫這個函數,它可能只傳入 座標值而已,但它可能不知道系統正處於彩色模示下,此時顏色color參數的預 設值便派上用場了。此外,有預設值的參數最好被放在參數列的最後頭,這樣預 設參數值才能發揮正確的效用。

8.6 雜要

關於C++與物件導向設計較有關的重點,就介紹到這邊。坊間關於C++的書不 少,請讀者自行參考。

不過,還是有些關係C++的有趣問題想提出來同諸君討論討論。在一般情況 下,指派(assign)變數值是很簡單的事,例如X=3等等,但類別的指派呢?一個 非常基本的作法是:重覆載入」=」這個運算子,當A=B時便將B案例中所有的成 員一個一個搬到A案例中。編譯器又是怎麼作呢?在C++ 1.2中,電腦是以一個一 個byte拷貝過去,而C++ 2.0則是採前面所提,遂次拷貝成員的方式。換句話說 ,編譯器「偷偷地」替我們多重載入了」=」這個運算子。1.2及2.0的作法各有 不同,為什麼2.0要改呢?這個問題留給讀者自行討論。

另一個有趣的問題是關於編譯器本身。C++最早沒有獨立編譯器。要編譯C++ 必須先用一個「前端處理器」將C++的程式碼轉換成C的原始碼,再交由C的編 譯器進行編譯的工作。造成這個有趣現象的原因是因為C++本身可被視為C語言 的巨集指令(MACRO)增強版。例如前面所提到的template就是一個明顯的例子。 又以轉譯class來講,前端處理器也只是將各類別的案例轉成「唯一名稱」的結 構(struct)而已。有興趣的讀者可以自己設計一個轉譯器,應該不會太難。

第九章 程式寫作的風格

一組好的程式編碼風格,對程式效率的影響雖然很小,但是在往後除錯,以 及維護階段來說卻很重要。因為程式碼不僅僅只是要給電腦執行而已,同是也必 需是可讀的、易於瞭解的。例如說註解應該寫明白,程式碼縮格、變數命名等等 。這些原則相信不論是任何教科書或是筆者以前的文章中都曾不只一次的強調過 。在本章中筆者不要重彈這些高調,筆者想介紹一個良好的範例。這個範例的來 源是Borland公司附加在TURBO C++/BORLAND C++中的CLASSLIB,類別程式庫。為 什麼要以CLASSLIB為例呢?筆者並沒有商業意圖,理由是因為它有明確的註解、 正確而良好的運用遺傳等等特質。這套程式庫的功能是提供一套標準化的資料結 構類別給予設計師,其中包含STACK堆疊、QUEUE佇列、ARRAY陣列、HASHTABLE雜 湊表、STRING字串......等等類別。以下舉其中的字串類別來做我們的範例,希望 各位能由以下的說明中,學習一套好的物件設計的風格。

9.1 String 類別的結構

雖然String字串類別不是一個資料結構實作的類別,而只是一個補強的資料 型態(因為C++ 並不內建字串的型別),但比起其它類別好講易懂,所以筆者挑 它出來討論。String類別本身由Sortable類別遺傳而來,Sortable所具的特性且 避不討論(因為Sortable也有它自己的功用)。String的軟體界面很符合要求, 茲分述如下:

class String: public Sortable
{
public: // 公共區,也就是String類別的界面部份
String( const char * );
String( const String& );
// 多重載入建構者

virtual ~String();

virtual int isEqual( const Object& ) const;
virtual int isLessThan( const Object& ) const;
// 以上這兩個函數是用來比較兩字串大小或相同

virtual classType isA() const;
virtual char *nameOf() const;
virtual hashValueType hashValue() const;
virtual void printOn( ostream& ) const;

String& operator =( const String& );
operator const char *() const;
private: // 私有區,String類別中受保護、不透明的內部資料
sizeType len;
// 紀錄字串的長度
char *theString;
// 字串的內容被存在這個字元指標中
};
表9.1 String類別的宣告內容列表


9.1.1 建構與破壞成員 (Constructor and Destructor)

String的建構成員是多重載入的,因為在String的案例產生時,也就是一個 字串變數開始其生命週期時,應該要指定一個文數字串給這個案例。各位可由此 瞭解一點,就是說不需要特別寫一個專門用來起始物件的成員函數。例如在此處 String類別的正確用法是:

String NewString ( &quotHello C++" );

而不必要用兩階段的啟始化。這樣就失去建構者的意義了,就像:

String NewString ;
NewString.init ( &quotHello C++" ); // String.init 是筆者假設的成員函數

那麼,為什麼建構者是兩個多重載入函數呢?在上面的說明中,String案例的啟 始都是直接指派一個字串給NewString物件,這是最直觀的用法,但在另外一種 情況中我們也會指派一個已經存在的String案例給它,這便需要用另一個建構者 了。那除構者有什麼用呢?話說建構者會向系統要求一塊空間來放置字串,而在 這個物件死掉的時後,便得要還回給系統,當然如果你自己設計一個相對於剛才 假定的init函數也可以,但讓物件自動收拾不是更好嗎?從這一小節我們可以瞭 解到當你設計一個物件時,應該善用建構者與除構者。

9.1.2 封閉的內部結構與開放的界面

String內部到底是怎樣子的呢?在私有區裡面儲存了兩個變數,一個整數變 數描述字串資料的長度,另一個字元指標則指向建構者所分配來儲存字串內容的 記憶空間。或許你覺得奇怪了,如果把字串資料放在私有區內,那我們不就無法 存取字串內容了嗎?是的,的確如此,但請看String的公共區,也就是它界面的 部份,有兩個主要的成員,一個是多重載入運算子"=",另一個是成員函數 printON(),多重載入運算子"="允許我們指派另一個字串案例的值,就像是A=B 這種方式,這樣便得以更改字串的內容;printON()成員函數則可以將字串的內 容輸出到指定的資料流(stream)上,藉此就可以取得字串的內容。那為什麼不把 字元指標直接放在公共區呢?這就是本小節討論的重點。在三章之前曾提到過資 訊隱藏的問題,在此有了一個很好的範例來詮釋。因為我們希望物件已外的的函 數或是程序不能夠隨意的改變物件的屬性,因為屬性若沒有作兩好的保護,是很 容易修改的。在String中有個變數是紀錄字串長度用的,試想只是改便字串內容 ,而長度資訊沒有隨著更新的話,是不是會造成意外呢?而且很明顯的,成員函 數不容易被破壞,而且透過函數運作來存取函數也可以同時箝制資料範圍。但還 有個多重載入的鑄型(cast)運算子是用來幹嘛的呢?這是用來將String的物件指 標轉換成字元指標,而其實它所轉換出的字元指標實際上是指向String類別中存 放字元資料的儲存空間,或許你會說:好啊,那我就可以自己直接修改字串內容 了,是的你可以,不過它在註解中卻聲明說最好不要藉此修改字串內容,因此這 項功能的本意僅只供提取資料用的。當然,String的介面可能還不是很齊全,但 這也提供了設計師一個很大的發展空間。

9.2 註解的使用

CLASSLIB註解的使用是我見過最週延的一種。它藉由註解將程式很明顯的區 隔開來。它使用註解的規格如下所示,很值得讀者參考:

標頭檔部份(header files)

// Content ---------------------------------------------
// 在這個部份描述這個模組內含有那些函數、類別、變數、結構等等
// Description
// 這個模組的主要功能概述
// End --------------------------------------------------

// Interface Dependencies ------------------------------
// 引進(#include)其它模組的原始碼
// End Interface Dependencies ---------------------------

// Implementation Dependencies -------------------------
// 引進外界模組定義的函數原型,就像#include &ltstdio.h>之類
// End Implementation Dependencies ----------------------

// Class // 定義使用者資料型態,如class、struct、union.....
// Description -----------------------------------------
// 這部份緊接在型態定義完畢後,詳細說明這個型態的用法,功能。
// 並且把每個成員函數、成員變數的意義用法說明白。
// End --------------------------------------------------

在程式碼的部份

// Function // 或是 member function, constructor等等
// 以下定義函數原型
.......
// Summary ---------------------------------------------
// 函數功能的簡介
// Parameters
// 參數的意義
// Return Value
// 返回值的意義
// Functional Description
// 函數功能運作詳述
// Remarks
// 追加的附註,警告等等資訊
// End --------------------------------------------------
以下就是函數主體了,當然內容也該要有註解
..........
// End Function // 函數結束,以下接續其它函數,也是同樣的格式

這麼詳盡的註解有什麼用呢?在CLASSLIB的說明文件就很大膽的說,關於各物件 的用法看原始碼就會懂了。這夠明顯了吧,一個易讀的程式碼本身就是一份很好 的說明文件,尤其在這種極端明白的註解下。

展望與結語

假定你是位程式設計師,物件導向的軟體設計可以替你省去不少工夫,帶來 更多便利。Clipper 5.0中也增加了物件導向的功能。記得在它的廣告中說: Clipper適合你的右腦。因為有人認為人的左腦掌管理性的思考,偏重於邏輯, 而右腦主掌人的創造力,因此藝術家的右腦往往來得發達,傳統的設計方式被那 些支持物件導向論的人抨擊為冷漠而不近人情。但就以筆者而言,用傳統方法寫 程式好幾年了,中毒也很深了,倒是無所謂。但是使用者呢?設計師在自己獲得 方便之餘是否也曾想過建立一個物件導向化的使用方法或使用者介面呢?

物件導向程式語言也並未發展完備。執行時期的動態繫結、自動衍生物件等 等功能都可能陸續出現。程式碼的最佳化也是努力的目標,因為與傳統語言比較 ,OOPL的效率明顯的偏低,顯然在最佳化方面還有相當大的努力空間。此外,我 們也需要 CASE(Computer-Aided Software Engineering)來輔助我們作物件導向 的設計工作,很幸運的,這些都正在發展中。

撰寫這篇文章帶給筆者很大的壓力。因為眼見許多人對物件導向的看法多有 所偏頗,整個大環境的發展也很畸型,大部份人都被程式語言牽著鼻子走,但卻 很少討論物件導向軟體工程論。捨方法論而就OOPL,真是捨本遂末的行為,且此 弊不見於今日,必為明日之患!智者怎能不杜漸防微呢?筆者預測未來五到十年 間,物件導向技術將會發展成熟,而必將在二十一世紀成為主流。

在此感謝鄭泰銘兄在我那潦草的稿紙中挑錯。還有李瑞老師及資訊社諸隸也 在此一併致謝。

參考資料
1. Rumbaugh, Blaha, Premerlani, Eddy, Lorensen. " Object-Oriented
Modeling And Design &quot. N.J.: Prentice Hall, 1991
2. Bjarne Stroustrup. " The C++ Programming Language 2nd Edition &quot.
Reading, Mass.: Addison-Wesley, 1991
3. 軟體工程 第二版, R.S. Pressman, 陳澤風譯, 松崗
4. C++ 程式設計入門, 黃美玉譯, 格致
5. Turbo C++ V1.0 CLASSLIB.DOC, Borland International.

Borland C++、Turbo C++、Quattro Pro是Borland公司的註冊商標
dBASE III、dBASE IV是安信達公司的註冊商標
Objective-C是Stepstone公司的註冊商標
Smalltalk-80是ParcPlace System的註冊商標

復刻版-序

拜免費經濟之賜,終於可以讓我也來搞搞網路出版了。
這個網站成立的緣起是因為小時候我寫了三篇文章投到建中青年,其中“物件導向的天空”以及“軟體設計的思維”兩篇在網路轉載之下,竟然奇蹟似地在 cyber-space 中存活了20年 !諷刺的是,因為不可靠的軟碟片,我自己手頭並沒有這些文章的原稿。為了讓這兩篇文章能夠再繼續存活20年,因此我選擇在這個時間點建立官方網站保存這篇文章。