前言或許以往學過物件導向的人,都知道封裝是把重要的實作包裝起來,以便向使用者隱藏細節,繼承實現可重用性,多形增加彈性,這些老生常談儘管也是物件導向的重點,但在實作階段我們似乎感受不到它的好處,反而不斷因為物件導向的複雜性吃了不少苦。
筆者必需說,許多這些討論都是見裡不見外的,它對於我們的參考性價值很低,因為這些說法只是見樹不見林,物件導向就像深邃的大洋一般廣闊無際,單以這些特性就想含括它所有內涵,只能說有點不自量力。
扎實的物件導向認知
我們對於物件導向的理解往往出於我們打從根基就沒有扎實的認知,許多人都將物件導向與可維護性.可擴充性.重用性畫上等號。
這是一個很不負責任的說法,因為它並沒有向讀者明確交待物件導向如何漂亮的實現這些特質,畢竟這些特質從程序式的觀點切入一樣適用。
事實上這些特性不僅僅是物件導向,所有語言也都以這些特性為終極目標,在程序導向程式設計,重用性可以靠子程序來解決,可擴充性,可維護性自然也不是物件導向獨有的特質,光就這些表面特質去探究物件導向,跟本無法看出物件導向的優勢。
若要有扎實的物件導向觀念,必需從實作角度去切入,如此一來,儘管它的面孔依舊深不可測,但我們能更看清楚它的全貌和輪廓。
一些顯而易見的實作特性
一、對於封裝的新理解
提供一個良好的命名規束準則
封裝能帶來的好處雖然很多,但是最直接的好處就屬命名,我們在開頭就開門見山跟大家講,跳過命名,基本上就等於摒棄.背離了美好曼妙的設計原則,在沒有物件導向的時代,良好的命名或許是一個痛苦的議題,因為當我們替資料跟函式命名之後,往往會發現這些資料跟函式有的時後很難替他們找到好的命名。
要替他們找到好的命名,往往就必需明確界定使用的界線跟範疇,但我們往往光是寫程式就很累了,如何有餘力去搞動這件事?這也是為什麼這些使用到共同特性跟共同資料的邏輯往往命名是模糊的......這個時後物件導向很自然的提供了這種規束,它不僅僅提供了一個良好的分類機制,使用物件導向把這些相關資料跟操作邏輯封裝起來的同時,也順便使用了良好的命名機制,例如......
cat.collor = vXX;
cat.meow();
減少函式的參數列大小
函式的介面在程式之中扮演著很重要的角色,過長的參數列會使得函式的介面溝通性變低,通常不建議一個函式用4個或4個以上的參數,這往往會讓程式設計師在回頭理解程式的過程中倍感困惑。
封裝在函式的介面簡化扮演著很重要的角色,它可以減少函式的參數數量,同時也增加函式介面的溝通性。
例如我這裡有一個位置的計算函式......
bool isXXX(float x, float y, diagram dragon)
{
.
.
.
}
我在這裡把參數列轉換成......
bool isXXX(center position, diagram dragon)
{
.
.
.
}
當然這只是從膚淺的層面看待它,物件導向的威力可不只如此,有的時後我們選擇直接讓實體資料傳入函式,會帶來一些尷尬的處境,這樣子萬一我必需新增或刪減參數列,我必需直接動到函式的介面,這會造成我每個該函式的引用點都要修改它,假如我系統內有20個引用點,我就要同時間修改 20 個引用點,但是若是我把參數以類別進行封裝,只要引用的類別進行這些修改就好,這樣就達成了簡單的局部化。
當然這還不是封裝最大的潛力,封裝最可怕的地方在於,它可以以非比尋常的效果根除重複,通常這些過長參數的函式有一個特性,你會常看到某些參數總是以一組一組的方式一起被傳遞,而且可能有好幾個函式都是這樣,這些參數組不僅僅造成介面的修改的局部化問題,更甚者,他們會在不同函式內產生共同的行為,如此便增加了很多的重複程式碼,造成 BUG 叢生。
前述我們講過物件導向的精隨在於把資料與資料相關的邏輯包裝在一起,從上述例子你可以看得出來......
bool isXXX(float x, float y, diagram dragon)
{
.
.
.
}
bool isXXX(center position, diagram dragon)
{
.
.
.
}
我把座標表示法以一個 center 類別表示,前後我不只把參數組封裝在一個類別,更可以把center 的操作特性也一併封裝在類別裡,這樣做有什麼好處呢......
如果我有10個函式要計算頂點間的距離,我可以把這些操作封裝在類別裡,如此一來,當這些函式使用相同的類別當作參數,就不需要把同樣的邏輯在不同的地方都複製一遍了,如此可以根除大多數不必要的重複。
除此之外,封裝隱藏了某些不必要曝露的細節,使得引用它的函式不會受這些細節的侵害。
二、重新認識多形
減少大量的條件陳述
配合繼承使用,多形最直接的好處就是減少重複,當條件陳述針對不同型別實作不同的邏輯段,多形配合繼承就有顯而易見的好處。
這段程式碼看似沒什麼大問題,不過仔細翻閱你的程式會發現,這樣的 switch 敘述或 if~else 敘述常常會在大型程式中好幾處重複出現,儘管它們出現的地方很多時後邏輯並不完全劃上等號,然而就是這一點才讓我們難以下手根除這種重複......
這個時後物件導向提供 overriding 的多形機制還有繼承機制,可以將RED.GREEN.YELLOW各自封裝成一個子類別,並繼承一個抽象類別或介面來實作子類別的操作,如此一來不僅讓重複的程式碼減少,當你要新增一個型別,只要往繼承體系新增一個子類別就好,不必更新所有引用點......
增加函式使用的彈性
好的名字有的時後是可遇而不可求的,若函式本身只是因為參數的不同.回傳值的不同而在命名上有所區別,只會白白浪費一個好名字,同類的操作,如果我只是梢作微調,多形的 overload 將提供這類彈性,當然這間接讓我們有辦法對同類函式的抽象概念有著一至的認知。
三、重新認識繼承
對類別的再分類與重用
繼承的重用在此不重提,因為繼承就是重用父類別定義的一些實作,但繼承當然不只如此,繼承最終的好處在於,我可以從父類別去擴展一些額外的分支,當我繼承這個父類別,就意味著,我有與它同等或相似的某些特性,並且實踐了他們。
比方說貓.狗.魚.熊貓繼承動物,從繼承關係可以考察到貓.狗.魚.熊貓這些類別的共通性。
一些額外特性支援繼承
1.隔離兩個實作區段
物件導向提供了一個很棒的機制來分隔兩個實作區段,那就是抽象類別跟介面,例如......
abstract class LightType
{
abstract int getLightType();
.
.
.
}
抽象類別或介面不會向使用者曝露完整的實作細節,它只會告訴我們:我的子類別實現了這樣的操作,所有實作細節由其背後繼承體系下的子類別來實現,而抽象類別則可以徹底包裝這些操作群集,其目的是讓使用者針對操作介面來寫程式,而不是針對實作來寫程式,也就是說這可以讓我的設計不會曝露在不該曝露的地方,如此可以有效保護使用者不受實作變化的侵擾。
四、物件導向的額外特性
分解過於龐雜的函式
過長的函式是危險的,它容易形成冗員跟濫權的的特性,而且難以達成軟體的紀律.溝通性.靈活性.簡單性,有大量重複的程式碼,在物件導向的設計下,一個超長函式很容易被拆成許多易讀性高的短函式,並且多了被其它子系統覆用的可能,減少了很多重複。