今年一月份的時候,筆者受命去為一家營造公司上課。課程的主題除了講解一般所謂的資訊系統為何物之外,還包括了講授何謂物件導向的程式設計,為時兩個小時。
本來以為這是一項輕鬆簡單的任務,但是沒想到老闆交代下來的客戶要求是上課內容不但要豐富精彩,而且重點必須是放在「物件導向的程式設計」上面。這下子筆者的臉上不但開始變得非常尷尬,且這一下子馬上將原本簡單的任務變成了「不可能的任務(Missing Impossible)」。因為在短短的兩個小時中,除了扣掉自我介紹、嚥口水、咳嗽、打噴嚏,以及段落之間的休止符之外,大概剩下不了多少時間。
所以要在這麼短的時間內介紹這樣的主題,筆者只好盡量撿一些重點來說明,也因此上課的結果是:筆者在台上比手劃腳,講的口沫橫飛,口水肆溢。台下則是問號掉滿了一地,大有丈二金剛摸不著腦袋之態。不過在準備教材的過程中卻讓筆者想起了兩段往事。
是定理還是方法
第一件事情是在1995年初,某天我的第一個老闆突然興沖沖跑來跟我說:「公司即日起開放員工申請訂購工作上所需的雜誌或書籍。這是申請單,你填一下吧。」我記得當時只填了一份高煥堂先生的「物件導向雜誌」。過了沒多久,老闆就一臉臭臭的回覆我說:「這本雜誌用的是VB作範例,而VB又不是物件導向語言,沒有參考的價值。」所以我的申請,就這麼被駁回了。
第二件事情是在 1995 年中時,筆者奉命參加中台灣地區的Delphi研討會。記得當時講師說了一段話讓筆者印象十分深刻。他說:「我們使用Delphi來開發應用系統,其實就已經在用物件導向的方式來寫程式了。因為在Delphi之中,我們幾乎都是在物件的事件中,使用物件的屬性以及方法來解決事情。」
這兩件事情跟本章的主題有何關係呢?其實關係大了,因為如果筆者詢問什麼是物件導向的程式設計?相信很多讀者都可以肯定的答覆筆者說:「物件導向的程式設計,有別於傳統水瀑式(Water Fall)的程式設計以模組作為思考基礎,而它則是以物件作為思考對象,進而進行應用系統的分割與應用。」
再熱心一點的會再接著告訴筆者,現在有哪些程式語言是支援物件導向,哪些則不支援。是的!這些答案都是蠻標準,但也蠻制式。不過基本上還是沒有說清楚什麼是物件導向的程式設計?只是假如答案是上述案例中講師所描述的那樣,那麼事實上恐怕也不太盡然。
物件導向的特性
根據Brad Cox於1980年所發表文獻中指出,所謂的物件導向應該具備下列的特性:
- ●物件(Object)以及訊息(Message)
- ●繼承(Inheritance)
- ●封裝(Encapsulation)
- ●動態連結(Dynamic Binding)
很驚訝吧!早在二十多年前,就已經有大師級的人物提出已經相當成熟的物件導向軟體設計的概念了。如果我們回溯那個年代,其實我們還可以發現很多令人驚訝的事情。比如1969年第一個純物件導向的程式語言Small Talk 被發表出來,那時候才正剛剛結束電晶體進入IC的時代而已。至於我們常用的C語言則是到了1971年才發表,而現在所用的PC則是一直到1981年才由IBM公司正式推出。
其中繼承、封裝、動態連結是物件導向中,最重要也具盛名的特性,所以很多人都據此來評斷,哪種語言或工具支援OO。我們常在一些論壇中見到諸如此類的敘述:「VB不支援類別(Class)的繼承概念,同時也不支援資訊封裝的方法,所以VB不是一個OO程式語言。Java由於不支援指標型態,因此更能徹底封裝類別的資訊,跟C++比較起來,更能貼近OO的精神。」
然而對於這些敘述,筆者姑且不討論他們之間的對與錯,只是覺得發表這些評論的人,把上述OO的特性當作是個「定理」來遵從罷了,所以才會造成論壇中每隔一段時間就會引起類似這樣話題的爭議。平心而論,在各家OO的先驅中對於OO的定義與規範並不盡相同,例如Brad Cox、Grady Booch、James Rumbaugh,及Ivar Jacobson等幾位大師所提出的概念就不完全一樣。如果讀者有興趣,不妨到圖書館查一查他們彼此之間也曾有過精彩的辯論。所以就各家程式語言中,對於OO所支援的程度有所不一也是理所當然,特別是喜歡自稱OO的先驅C++都是如此。
物件導向定義的驗證
假如我們硬是以某一種語言作為標準來評斷其他的語言,在作法上並不公平。因此為了證明這一點,筆者根據Brad Cox所公布的OO特性,在此大膽的進行一項有趣的驗證:
首先我們假設「凡是支援繼承、封裝、動態連結的程式語言或工具,我們可以說它們是支援 OO 的,反之則亦然」。然後為了證明這個假設能夠成立,我們先對剛剛所謂的OO特性進行以下的定義:
- ●繼承 → Source的Reuse。
- ●封裝 → 保護程式中的資訊,並透過驗證過的程序來做存取。
- ●動態連結 → 於執行時期決定執行物件的種類與分法。
現在筆者以繼承和封裝為例,並以素有OO之稱的Delphi與不支援OO的VB兩種工具,來逐一比對它們之間的差異。
SayHello的函數範例
unit Main001;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TA = class(TObject)
public
procedure SayHello;
end;
TB = class(TA)
public
procedure SayHello;
end;
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
﹛Private declarations﹜
public
﹛Public declarations﹜
end;
var
Form1: TForm1;
implementation
﹛$R *.dfm﹜
{ TA }
procedure TA.SayHello;
begin
ShowMessage('Hello by Class A ...');
end;
{ TB }
procedure TB.SayHello;
begin
Inherited;
ShowMessage('Hello by Class B ...');
end;
procedure TForm1.Button1Click(Sender: TObject);
var Obj: TA;
begin
Obj := TA.Create;
Obj.SayHello;
Obj.Free;
end;
procedure TForm1.Button2Click(Sender: TObject);
var Obj: TB;
begin
Obj := TB.Create;
Obj.SayHello;
Obj.Free;
end;
end.
在上面的範例中,我們可以看出其中簡單直接的繼承關係,類別TB繼承自類別TA。其中類別TB的成員函數SayHello只不過是從一個函數再去呼叫另一個函數而已,所以當呼叫類別TB的成員函數SayHello時,它會分別顯示「Hello by Class A ...」以及「Hello by Class B ...」兩個訊息。因此如果單單從這個角度來看的話,VB 也是可以做的到。至於繼承中的負載(Override),我們只要在 VB 中加上一些遮罩用的旗標,一樣可以達到相同的效果。
資訊封裝範例
TC = class(TObjct)
private
FCaption: String;
function GetCaption: String;
procedure SetCaption(const Value: String);
public
property Caption: String read GetCaption write SetCaption;
end;
上面是一個資訊封裝的例子,其中屬性Caption必須透過GetCaption與SetCaption這兩個方法才能存取得到,目的在於保護FCaption不會被外界任意的存取,並且保證FCaption所儲存的資料會經過一定程序的驗證。現在我們再看看下面VB的程式碼。
VB的cls檔的範例
Option Explicit
Private pMember() As String
Private pCount As Integer
Private Sub Class_Initialize()
pCount = 0
End Sub
Public Property Get Count() As Variant
Count = pCount
End Property
Public Sub Add(vNewValue As String)
pCount = pCount + 1
ReDim Preserve pMember(pCount - 1)
pMember(pCount - 1) = vNewValue
End Sub
Public Sub Clear()
pCount = 0
ReDim pMember(pCount)
End Sub
Public Sub MoveItem(vIdx As Integer)
Dim i As Integer
For i = vIdx To UBound(pMember) - 1
pMember(i) = pMember(i + 1)
Next
pCount = pCount - 1
ReDim Preserve pMember(pCount)
End Sub
Public Function IndexOF(vSearch As String) As Integer
Dim i As Integer
IndexOF = -1
For i = LBound(pMember) To UBound(pMember)
If vSearch = pMember(i) Then
IndexOF = i
Exit Function
End If
Next
End Function
Public Function Item(vIdx As Integer) As String
Item = pMember(vIdx)
End Function
Public Sub SetItem(vIdx As Integer, ByVal vNewValue As Variant)
pMember(vIdx) = vNewValue
End Sub
以上是筆者用VB寫的一個cls檔(即是 VB 的物件類別模組),用以實做一個TsingleList。雖然它不是所謂的Class形式,但是透過private與public的宣告,一樣具有所謂資訊封裝的效果。換言之,別的VB單元是無法存取到private的內容。
Delphi的動態連結的範例
procedure TForm1.Button1Click(Sender: TObject);
begin
TWidgetControl(Sender).Enabled := not TWidgetControl(Sender).Enabled;
end;
上面是Delphi一個動態連結的範例,我們可以看到參數Sender在編輯時時,並未確定它的真正型態,反而是到了執行時期,我們才藉由TWidgetControl類別取得Enabled的屬性值。這樣的效果在 VB 中,用不定型態、物件變數或利用COM的特性一樣可以達成。我們可以運用物件變數,如Object或Control來達到這樣的結果:
物件變數的範例
Public Sub EnableControl( Sender as Object)
Sender.Enabled = not Sender.Enabled
End Sub
此處若只是要檢查型態,只需運用Typeof或TypeName來確認型態即可,因此我們可以對上面的程式做些修改:
檢查型態的範例
Public Sub EnableControl( Sender as Object)
If TypeName(Sender) = "TWidgetControl" then
Sender.Enabled = not Sender.Enabled
End If
End Sub
從上面的推演過程中,相信筆者的假設獲得成立的有VB可以支援繼承、封裝,及動態連結。換句話說VB是支援OO的程式語言。這樣的說法相當弔詭,而且在理論基礎上也有些牽強附會。不過本文撰寫的目的,不是要賣弄詭辯之術,而是要藉此機會向各位讀者說明其實物件導向程式設計,是一種約定成俗的設計方法,和一種推理歸納的思考模式,而不是一種條文式定理。不過這樣說實在太模糊了,我們還是用範例來說明吧!
不同作法的物件導向架構
(圖一)的範例是一個含有三組以三個Button作為Group的程式,當使用者按下第一個按鈕時,會Disable本身並會Enable第二個按鈕,同理其他的按鈕也是如此。這樣的作法,在於讓使用者依次序來按下Button,而不讓有次序不同的操作出現,這種次序性安排方式在很多防呆的應用中是經常看得見。在這個範例之中,筆者提供了三種不同的撰寫方式,現在我們來看看它們的程式碼是如何撰寫:
第一種範例,分別在各自Button的OnClick事件撰寫處理的程式碼。
procedure TForm1.Button1Click(Sender: TObject);
begin
Button1.Enabled := False;
Button2.Enabled := True;
Button3.Enabled := False;
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
Button1.Enabled := False;
Button2.Enabled := False;
Button3.Enabled := True;
end;
procedure TForm1.Button3Click(Sender: TObject);
begin
Button1.Enabled := True;
Button2.Enabled := False;
Button3.Enabled := False;
end;
第二種範例,在某一個Button的OnClick事件中撰寫處理程序,然後其他Button的OnClick事件再指向這個處理程序。
Button4.Enabled := False;
Button5.Enabled := False;
Button6.Enabled := False;
if Sender = Button4 then Button5.Enabled := True;
if Sender = Button5 then Button6.Enabled := True;
if Sender = Button6 then Button4.Enabled := True;
第三種範例,我們撰寫了一個TButtonGroup新的類別,專門來處理按鈕順序的問題。
type
TButtonGroup = class(TGroupBox)
private
Button1: TButton;
Button2: TButton;
Button3: TButton;
procedure OnClick(Sender: TObject);
public
constructor Create(Owner: TComponent); override;
destructor Destroy; override;
end;
constructor TButtonGroup.Create(Owner: TComponent);
begin
inherited;
Button1 := TButton.Create(Self);
Button2 := TButton.Create(Self);
Button3 := TButton.Create(Self);
Button1.Parent := Self;
Button2.Parent := Self;
Button3.Parent := Self;
Button1.Name := 'Button7';
Button2.Name := 'Button8';
Button3.Name := 'Button9';
Button1.Top := 25;
Button2.Top := Button1.Top + Button1.Height + 10;
Button3.Top := Button2.Top + Button2.Height + 10;;
Button1.Left := 28;
Button2.Left := 28;
Button3.Left := 28;
Button2.Enabled := False;
Button3.Enabled := False;
Button1.OnClick := OnClick;
Button2.OnClick := OnClick;
Button3.OnClick := OnClick;
end;
destructor TButtonGroup.Destroy;
begin
Button1.Free;
Button2.Free;
Button3.Free;
inherited;
end;
procedure TButtonGroup.OnClick(Sender: TObject);
begin
Button1.Enabled := False;
Button2.Enabled := False;
Button3.Enabled := False;
if Sender = Button1 then Button2.Enabled := True;
if Sender = Button2 then Button3.Enabled := True;
if Sender = Button3 then Button1.Enabled := True;
end;
procedure TForm1.FormShow(Sender: TObject);
begin
BG := TButtonGroup.Create(Self);
BG.Parent := Self;
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
BG.Free;
end;
從上面的範例中,我們可以清楚地看到雖然程式碼都是由Delphi來撰寫,所表現的行為也相同,但很明顯的是第一種方法非常不高明,因為它把程式碼分散到每一個物件的OnClick事件中來處理。假如這時所需要的物件不多,那麼還感覺不出來有任何的問題,但是一旦需要控制的物件很多,那麼整支程式豈不是到處都以這種使用者控制方式的程式碼,這豈非是不智之舉,所以我們歸類這種寫法是屬於新手級的寫法。
第二種方法雖然還是需要在程式之中撰寫控制程式碼,但明顯地就比第一種方法簡潔許多,基本上我們歸類這種寫法為老手級的寫法。因為這樣寫法已經具備模組化的觀念,所以程式可以寫的十分地有條不紊。
第三種方法我們稱之為物件導向式的寫法,因為這種寫法已經將這種類似物件操作的控制程式碼,轉移到新增的TButtonGroup類別中,所以主程式裡面就可以完全不用理會那些Button之間要如何運作,讓程式看起來更為簡潔有力。
從上面的範例中,相信聰明的讀者已經感受到同樣的程式,及不同寫法之間的差異了。雖然這些差異雖然對於使用者而言,完全感受不到它們之間的不同所在,就像不管白貓或是黑貓,也是只要會抓老鼠的貓,就是好貓,但是對於程式員在實作上,這些差異影響就很大了。因為一支有條不紊的程式,畢竟總是比較容易維護。當然對此有些人可能會不服氣,並且強調這是個人程式撰寫的風格問題。但是筆者要強調的是撰寫程式原本就是希望要迅速有效的為客戶解決問題,而不是在玩「記憶金庫」這樣的遊戲。更重要的是程式要讓別人看的懂,這樣才有辦法進行團隊的合作,提升軟體的價值。而使用OO也正是使用它所提供方法來達成這樣的目的。
討論到這裡,我們可以回應本文之初所描述的那兩件事情,那就是雖然VB普遍不被人認為是屬於OO的程式語言,但事實上使用VB,我們還是可以很OO。同理,既使我們用的是OO的程式語言,例如C++、Java等程式語言,一樣可以寫出很不OO的系統出來。其中的關鍵,就是程式員是否真的具有OO的概念。
(圖二)是筆者以前用VB所撰寫的專案。請留意畫面右方專案清單裡的檔案型態,其中除了一般的vbs,以及frm類型的檔案之外,還多了cls類型的檔案,且筆者還故意的使用類似Delphi對於類別命名的慣例,在檔案名稱之前加上『T』這個字母,其作法就是仿照OO中,對於類別的定義。至於VB所編譯出來的程式,以前在工程師間一直被詬病有不穩定的傳聞,但是這支程式在客戶使用的這些年以來,一直都很穩定。這證明了不管使用何種語言,只要採用一個好的方法,的確可以產生一個穩定的程式,因此有誰能說VB不好呢!
@大標:物件導向的思考模式
記得筆者剛剛所說的物件導向程式設計是一種設計的方法,也是一種思考的模式嗎?剛剛筆者的演繹,只是示範了物件導向的設計方法,而物件導向的思考的模式,則又是怎樣的一回事呢?我們以一個範例來說明:
現在我們預備撰寫一部西遊記的遊戲,其中的角色包括唐三藏、孫悟空、豬八戒,以及妖怪等。然後在遊戲之中會遇到過河,或遇到妖怪等事件,在傳統的思考模式之中,系統通常會用切割成下面的樣子。
- 1. 西遊記主程式。
- 2. 過河。
- 3. 遇到妖怪。
- 4. 妖怪遇到三藏師徒。
其中過河部分會包括三個步驟:
- a. 如果唐三藏過河則划船。
- b. 如果孫悟空過河則用飛的。
- c. 如果豬八戒過河則用游的。
而三藏師徒遇到妖怪也會分成三個步驟:
- a. 如果唐三藏遇到妖怪則念經。
- b. 如果孫悟空遇到妖怪則斬妖。
- c. 如果豬八戒遇到妖怪則大喊救命。
而妖怪遇到三藏師徒也會分成三個步驟:
- a. 如果是唐三藏則吃掉。
- b. 如果是孫悟空則逃跑。
- c. 如果是豬八戒則戲弄他。
然而在物件導向的思考模式下,系統通常會用切割成下面的樣子。
- 1. 系統主程式。
- 2. 唐三藏。
- a. 過河則划船。
- b. 遇到妖怪則念經。
- 3. 孫悟空。
- a. 過河則飛。
- b. 遇到妖怪則斬妖。
- 4.豬八戒。
- a. 過河則游。
- b. 遇到妖怪則呼救。
- 5. 妖怪。
- a. 遇到唐三藏則吃。
- b. 遇到孫悟空則跑。
- c. 遇到豬八戒則戲弄。
聰明的讀者看出上面範例中的不同點了嗎?很明顯在傳統的方式裡雖然模組化可以將程式規劃的很清楚,但是免不了每一個模組都必須跟全域變數或物件或模組糾纏在一起。這樣的結果,在團隊的開發中是非常不利的,因為除了在系統開發之前,系統負責人需要將所有溝通的介面規格制訂的清清楚楚之外,任何人、任何模組在事後的所做的任何修正都會影響到系統的正確性,這正可謂之『牽一髮而動全身』而物件導向的模式則可以避開這樣的問題。
另外還有一個重點是在OO的思考模式裡,我們不但可以順利切割整個應用程式的程式碼部分,就連程式中的邏輯部分,也一併切割的清清楚楚。這是一個在開發應用系統中,不容易出錯的最主要關鍵。
結論
討論到這裡,很多人都會問為什麼我們要採用物件導向程式設計(OOP)的方式來設計系統呢?其實深究起來原因很簡單,答案就是要儘量減少系統開發的時間以及出錯的機率。然而很遺憾的,在筆者接觸許多國內的案例中,雖然他們都是使用Java、Delphi或C++,但卻鮮少有人真正使用物件導向的思維來設計程式。更有甚者連基本模組化的工作都做不好,大有人在。
這是很令人痛心的現象,因為這樣的結果,將導致國內的資訊市場中,充斥一些不健全的系統,而在劣幣驅逐良幣的情況下,國內將根本無法擺脫軟體代工的命運。而所謂的資訊人,或許說是有機會看到本篇文章的人而言,也會因為環境的因素,逐漸放棄應有專業素養,進而演變成一頭受過高等教育但是只會拉磨(編寫程式碼)的驢子而已。猶記得位大師曾經說過一句話:「程式碼的價值,不在於作者是否使用任何奇幻的技巧,而是在於它的臭蟲是最少。」筆者在只是想藉由本文傳達一個訊息,那就是其實我們還有更好的選擇,可以讓我們的未來更加美好而已。
<作者聯絡方式:mhwul@pchome.com.tw>
|
|
物件導向的概念不只用在一般的電腦或資料庫裡,也可用在Modeling的製程裡,而這個階 段的工作便稱為「物件導向分析」(Object
Oriental Analysis),又名「視覺塑型分析」(Visualizing Modeling
Analysis)。「物件導向分析」代表從分析階段就開始建立正確的物件導
向概念,而「視覺塑型分析」則彰顯分析在此是一種視覺化與模型化的過程。相關介紹請見「物件導向分析概略」一文。 |
|
歷經千辛萬苦所設計出來的程式,為何總是無法被重複使用呢?通常有兩個原因。第一、
這些產品知識只存放在某些成員的腦袋裡。第二個原因則是這項產品本身就不具備「可回收利用」
特質,要不成為垃圾也難。要解決第一個問題,必須仰賴一整套的軟體開發制度與標準作業規範。
至於第二個問題,則可以用物件導向的分析與技術來做為解決的方案。你可在「程式設計也要有「環保」概念」一文中得到進一步的介紹。 |
|
一個新世代IC設計資料庫大部分的內容,涉及IC設計中用以完成目的(intent)與履行(
implementation)的綜合資料模組。而具備的高效能、物件導向API等特質的設計資料庫,更可讓應
用程式開發人員在無須其內部履行細節的情況下輕鬆使用。在「新世代IC設計資料庫的開放與互通」一文為你做了相關的評析。 |
|
|
|