在三國演義中第一回曾開宗明義的提到『話說天下大勢,分久必合,合久必分。周末七國分爭,並入於秦。及秦滅之後,楚、漢分爭,又並入於漢。漢朝自高祖斬白蛇而起義,一統天下;後來光武中興,傳至獻帝,遂分為三國。』這說明了歷史上的分分合合乃是自然之勢。同樣的這樣情形在軟體界也是一樣的!?所以如果您曾經留意,其實您可以發現,軟體的發展從一開始單純的應用程式(統一的狀態),走向分散式元件模式如DCOM、CORBA等(獨立的模式),然後現在再走向整合性 .Net 的模式(統一的狀態)。
然而這個現象跟物件導向技術有什麼關係呢?其實早在1968年就出現了物件導向的語言Simula68,然而卻直到相隔將近二十多年後的1986年,由全祿研究中心(Xexor PARC)出版了兩本有關Smalltalk語言的書後,才讓物件導向技術獲得重視。這其中被咸認為最主要的理由就是在於如C及Pascal等結構性語言,已經無法應付現今越趨複雜的視窗程式、主從式(Master-Detail)架構、多層次(Multi-tier)架構等系統的程式。
可是話又說回來,自從物件導向技術被獲得重視之後,大多數的人都相信物件導向技術可以解決掉軟體開發上大部分的問題,但是,我們從現今物件導向技術使用的範圍被侷限於開發工具之中,而真正的使用者案例(User Case)卻少見的情況來看,這個答案似乎又不能被肯定了。這其中的關鍵,筆者認為問題就是出在統獨的演進之下,所衍生的問題不能夠被有效的解決。為了解釋這個論點,在接下來的章節中筆者將以實際的案例作進一步闡述。不過在這之前我們先來看一段故事:
大同電視的故事
- 「話說三十年前,我們家的幾個小蘿蔔頭一到傍晚就會到隔壁鄰居報到,為的是準時收看六點鐘的卡通。雖然偶而可以混一頓晚餐吃,但是畢竟牽扯到顏面問題,筆者的老爹決定為家裡面添購一台大同電視。
- 還記得那時家裡第一台電視被送來的模樣,木製落地式可左右旋轉的外殼,與洗衣機定時器外觀類似的選台器,以及一個可以將螢幕遮蔽的對開百葉拉門。比起鄰居的老電視,其樣式可謂之豪華極了,讓當時幼小的心中可以說是充滿了一片虛榮。
- 數年後電視機螢幕出現雪花及影像扭曲的現象,於是我們請服務站的技師來維修,而我們幾個小蘿蔔頭則擠在技師後面很熱心的幫倒忙,還記得當時拆下電視機後方的甘蔗板後,只見裡面的電路板上排列了許多燈泡(其實是真空管)以及密密麻麻的線路。年輕技師這摸摸、那瞧瞧,弄了半天一頭大汗還是沒搞定,於是他決定回去搬救兵。沒多久來了一位老師傅,只見他瞧了螢幕一眼後就跟年輕的技師說:『遇到這種問題,你只要在某某塊電路板上,某個電容器上用一個小螺絲起子調整一下就可以了』果然經過這麼一下校正之後,電視就回覆正常了。頓時讓站在老師傅後邊的技師,以及我們幾個小蘿蔔頭臉上升起一副敬佩的神色。
- 後來在這三十年中家裡陸續換購幾台電視,而且只要發生故障,家裡都會依例請服務站的人來維修,只是維修的方式,不知不覺中慢慢的開始有些變化。剛開始的時候維修的技師會帶著大大的工具箱,然後憑著經驗拿著電表慢慢的找,接著再用電焊槍換掉有問題的零件,所以常常維修一個案子需要大半天的時間。後來變成維修的方式改成直接更換電路版或者是整個模組,這樣的方式,維修的時間就消耗很少了。而到了近年隨著電子類產品的可靠度的大幅提昇,維修保固的方式起了巨大的變化,變成直接更換新品,或者是直接建議報廢。
- 因此如果說這三十幾年來我們透過電視見證了阿姆斯壯登月的過程、越戰後的慘劇、以及對棒球的狂熱,則同樣透過大同服務站的技師們,我們也見證了整個電子設備的發展史從真空管時代、演進到電晶體、然後再從電晶體時代演進到積體電路(IC)的時代歷程。」
看完上面的故事後,我們大略可以領略到一個事實,那就是電子產業在進入IC時代後,所發生的巨大變革 - 產品開發週期縮短、以及功能上的越趨於豐富多變。那麼也許會有人這樣問為什麼這樣的演進呢?這因為必須歸功於IC設計上的兩大特性,那就是所謂的「獨立性」與「可靠度」。其中「獨立性」使得各模組之間可以任意的組合,「可靠度」則大幅減少系統整合測試的時間。但是反觀在軟體設計的領域中,即使使用的是物件導向技術,這樣的特性也是並不常見,這是為了什麼?為了探索這個問題,我們需要先談一談這其中的概念與架構。
角色與場景的交互干擾
曾經有句話是這麼說的:「在物件導向的世界裡,凡事皆為物件。」沒錯,這句話的確很貼切的描述了物件導向中的精神,所以我們在很多書籍或者是開發工具中,都可以見到他們是如何的教導人們去規劃設計物件的結構(Construct)、建構它們之間的關係(RelationShip),並且更進一步的依此發展出模版(Pattern)的概念。然而可惜的是對於大多數的人而言,這些技術理論的概念並無法獲得實際的應用,主要的理由則在於程式設計師們大多注重於塑模的建構,以及程式設計的技巧,卻忽略了最重要的「環境」這項因素,以致於在系統愈漸龐大的時候,便發生邏輯切割上的困難,這怎麼說呢?我們且先看看下面的一段例子:
第二十二回 八戒大戰流沙河 木叉奉法收悟淨
- 「話說唐僧師徒三眾,行過了八百黃風嶺,正行處,只見一道大水狂瀾,渾波湧浪。忽見岸上有一通石碑。上有三個篆字,乃流沙河,師徒們正看碑文,只聽得那浪湧如山,波翻若嶺,河當中滑辣的鑽出一個妖精,十分兇丑: 一頭紅焰發蓬松,兩只圓睛亮似燈......
- 那怪一個旋風,奔上岸來,逕搶唐僧,慌得行者把師父抱住,急登高岸,回身走脫。那八戒放下擔子,掣出鐵鈀,望妖精便築,那怪使寶杖架住。他兩個在流沙河岸,各逞英雄。這一場好斗: 九齒鈀,降妖杖,二人相敵河岸上......
- 他兩個來來往往,戰經二十回合,不分勝負...那大聖打個忽哨,跳到前邊輪起鐵棒,望那怪著頭一下,那怪急轉身,慌忙躲過,逕鑽入流沙河裡。......行者笑道:『賢弟呀,這樁兒我不敢說嘴。水裡勾當,老孫不大十分熟。若是空走,還要捻訣,又唸唸避水咒,方才走得。不然,就要變化做甚麼魚蝦蟹鱉之類,我才去得。若論賭手段,憑你在高山雲裡,幹甚麼蹊蹺異樣事兒,老孫都會,只是水裡的買賣,有些兒榔杭。』八戒道:『老豬當年總督天河,掌管了八萬水兵大眾,倒學得知些水性......』說聲去,就剝了青錦直裰,脫了鞋,雙手舞鈀,分開水路,使出那當年的舊手段,躍浪翻波,撞將進去,逕至水底之下,往前正走。
- 卻說那怪敗了陣回,方才喘定,又聽得有人推得水響,忽起身觀看,原來是八戒執了鈀推水。那怪舉杖當面高呼道:『那和尚那裡走!仔細看打!』八戒使鈀架住,二人整斗有兩個時辰,不分勝敗。這才是銅盆逢鐵帚,玉磬對金鐘…...
在上面的例子中我們看到孫行者其實並不是法力無邊的角色,雖然在陸上、空中他是一個令人敬畏的大師兄,但是到了水裡之後,作戰能力甚至不如八戒。為此,我們在設計系統的時候要怎樣詮釋孫行者在各種場景,與各種妖怪對應的情況呢?針對這個問題很多人會習慣使用「旗標」作為一個狀態的識別,但是如果系統中物件種類一旦繁瑣起來、而且橫跨的場景又是犬牙交錯的情形,那麼系統中光是狀態旗標的維護,恐怕就是一件巨大的工程,更遑論要建立物件之間的關連或者是模型了!這也就是許多應用軟體在發展成大型系統時所面臨的重要關鍵所在。
換句話說,也就是當系統逐漸龐大之時,我們可以發現對系統切割的困難,便來自於物件的「獨立性」開始降低,「相依性」逐漸上升的緣故。而具體的影響就是物件之間相互的干擾,我們以下面的例子來解說,圖一的左方是由筆者所設計的一個 TImageListBox 類別,作用在於以清單的方式顯示圖片,並且讓使用者可以瀏覽並挑選適當的的圖檔。在這個例子中,筆者簡單的以 TScrollBox 作為我所要表達的容器類別(Container Class),然後再以 TImage 作為我所要的顯示細項。
《圖一 以清單方式顯示圖片的TImageListBox 類別》 |
|
下面的程式碼是 TImageListBox 的部分內容,在這個類別之中,筆者為了讓使用者在挑選的過程中具有標示(Mark)的功能,因此讓 TImage 之間共用 TImageListBox 的 OnClick 與 OnDblClick 事件,並且將此等關係(RelationShip)發佈(Published)出去,使之成為TImageListBox 的 OnSelectDblClick 事件觸發機制。
TImageListBox = class(TScrollBox)
private FList: TList;
procedure OnClick(Sender: TObject);
procedure OnDblClick(Sender: TObject);
…….
Public
Procedure Clear;
property OnSelectDblClick: TNotifyEvent read FOnSelectDblClick write FOnSelectDblClick;
end;
procedure TImageListBox.Add(Item: TImageListItem);
begin
Item.OnClick := OnClick;
Item.OnDblClick := OnDblClick;
FList.Add(Item);
end;
procedure TImageListBox.Clear;
var i: Integer;
begin
for I := FList.Count-1 downto 0 do
TComponent(FList.Items[i]).Free;
FList.Clear;
end;
…..
procedure TFmPickAPForm.ImageListBox1SelectDblClick(Sender: TObject);
begin
ImageListBox1.Clear;
end;
上述的程式碼中所陳述的技巧是大多數程式設計師所慣用的架構之一,它的好處是架構簡單而且脈絡十分清楚,並且大部分的情況之下,可以滿足多數的需求。但是,這裡面卻隱藏了一個致命的錯誤,那就是當使用者於TImageListBox 的 OnSelectDblClick 事件中呼叫它的 Clear 方法,此時系統便會出現「存取不當記憶體」的錯誤訊息。為什麼會這樣呢?現在您回頭仔細觀察上面的程式碼,您可以發現問題其實是出在 TImage 與 TImageListBox 之間所建立關連之上!原來當我們雙擊TImage之時雖然可以順利觸發筆者所設計的TImageListBox中的OnSelectDblClick事件,但是就編譯器的角度來看這個過程只是由一個函式之中,將參數推入堆疊,然後再呼叫另一個函式。此時若以Clear的方法清除了清單資料,則當程式完成OnSelectDblClick事件的處理函式之後,雖然可以讓程式的執行點回到正確的起始位址,但這個位址卻早已不是程式的控制範圍,所以會出現「存取不當記憶體」的錯誤訊息是理所當然的。
在上面陳述中我們雖然是展示一個很簡單的範例,但是卻足以顯示物件與其容器物件之間互相干擾的情形!至於干擾來自於物件之間並非「獨立」的關係,因此解決之道,則是您必須移除他們之間的任何關連,而非如孫行者的例子中設定個旗標就可以解決的。不過對於經驗豐富的設計師來說上述的干擾現象,只不過是在眾多系統設計實務過程中所遭遇到的問題的冰山一角而已,而且這些問題大多都很難以程式技巧來加以解決。最著名的一個例子如資料庫的「死鎖」(Dead Lock)問題,便來自於多使用者(Multi-User)或多執行程序(Multi-Process)所造成的。因此系統管理者多會以藉助規範使用者的操作,來規避這個問題,但是眾所皆知的這種方式在實務執行上通常不是很順利的。
從上面的陳述中,相信讀者已經可以認同程式的獨立性,對於系統的穩定度有著莫大的影響!因此對於熟練的系統分析師而言,在進行系統模型塑造之時莫不致力於各模組之間的邏輯切割,務求提升模組的獨立性,以期降低相互間的干擾。
然而可惜的是這樣的要求通常是不容易做到的,為什麼呢?如果我們回顧一下先前大同電視的例子,並且假定說IC的「獨立性」與「可靠度」的特性來自於IC的封裝,使得其內含的邏輯運算不得不取決於其接腳(Pin)的電氣信號,那麼在理論上軟體的發展也應該作的到這一點。
嗯,沒錯!關於這一點我們的確可以在一些現今的產品上看到其類似的蹤跡,例如Java與.Net等取消了對於指標(Pointer)的支援,或者強迫程式設計師使用類別(Class)等的作法,就是一個很明顯的例子。但是事實上我們遵循這樣的機制之後,果真可以幫我們解決「獨立性」的問題嗎?
如果我們回到上面TImageListBox的簡單範例中,就清楚的看到即使我們在SA/SD乃至於Programming的的過程中全部採用OO的技術及理論,卻依然不容易讓一個程式設計師在設計之初或除錯的階段即避免掉這個問題?這其中的主要關鍵便出自於程式的例外(Except)發生於非設計時期(Design Time)的預期(註1.)以及錯誤訊息的辨識困難(註2.),而這些問題通常必須仰賴程式設計師個人的經驗與功力來加以解決!
結論
於是這便衍生出一個焦點,那就是我們學習及利用物件導向的概念及方法,有辦法或者是機會解決這個問題嗎?答案是有的!不過限於篇幅,筆者以後會另文一一帶出這些方法,如果讀者對此有興趣的話,請耐心等候!然而在這之前筆者需要回應文章之初筆者所提的「環境」問題。
這裡所謂的「環境」泛指的是程式運行所需的條件,但是關於這類問題在Microsoft的作業系統從DOS升級至Windows後就鮮少在程式設計師的考量範圍之內,特別是在很多開發工具提供IDE的環境之後更是如此。於是這些先進的開發工具雖然可以有效的降低程式設計師的入門門檻,但是卻也是推動物件導向程式設計上的最大阻礙!
我們以上述的TImageListBox為例,如果您想要設計成獨立的元件,並且可以快速的除錯,光是熟悉Delphi所提供的工具是不夠的,因為您還需要知道Delphi的VCL(Virtual Component Library)運作方式才有辦法追蹤錯誤的所在、您必須清楚知道資料結構(Data Structure)的設計原理,才能掌握這些VCL的設計脈絡、您必須熟悉編譯器理論(Complier Theorem)才可能知道錯誤發生的原因,此外您還需要瞭解Windows作業系統(Operation System)的運作模式,才能讓您擁有解決方案的依據。而這些種種的背景知識(Background Knowledge)才是您真正設計獨立程式碼的關鍵所在!所以如果我們說「設計獨立的程式碼」是系統整合前的預備工作,那麼充實這些背景知識,就應該是程式設計師入門的基本功了。
注釋
註1:預期例外:這是一個程式設計中的一個重要技巧。所謂的「預期例外」指的是程式運行的過程中,預期某個例外(錯誤)有可能發生之意。因此在Delphi中我們通常會以try ... except ...end; 或者 try...finally...end; 等語法將這段預期可能發生錯誤的程式碼加以包裹,例如I/O的存取或者是網路的逾時處理等以避免系統的當機。
註2:對於Windows的程式設計師而言,藍底白字的錯誤訊息是一個可怕的夢魘!那除了表示著系統正在當機的狀態之外,更意味著程式中隱含著不知伊於胡底的臭蟲!雖然近來這樣情形有所改善,但是顯示「存取不當記憶體」的錯誤訊息,或者是組合語言式的除錯工具,對於工程師的除錯工作依然是沒多大幫助的!
(作者聯絡方式:mhwul@pchome.com.tw)