大李沒告訴我接口與抽象類的區別,什麼時候用接口,什麼時候用實現繼承。 弄得我中飯也沒吃好,老在琢磨這事,這不,一吃完飯,我就沖上樓,一個房間 一個房間轉,到處找大李。
過了好一陣,這老哥才和幾個同事說說笑笑地 回到辦公室,我立即走上前,半請半拉地把他拽到電腦旁。“大李哥,我實 在想得頭暈,既然在VB.NET中接口有了這麼大的發展空間,在形式上與抽象類如 此相似,那麼它們有什麼區別?什麼時候用接口呢?”
聽著我一連 串的問題,大李微笑著搖搖頭,拍拍我的肩膀說:“小兄弟,不光是你弄不 清,其實就是很有經驗的程序設計師也對什麼時候用接口,什麼時候用抽象類而 頭痛咧。”
此話一出,我更是疑惑重重。不過反而安下心來,老鳥 們都弄不清的問題,我不清楚也不必心中不安了。哈……
大 李看著我忽憂忽喜的表情露出了一絲詫異,不過他沒有理會,繼續說:“但 是這個問題我們還是有必要好好分析一下,讓我們更明白接口與抽象類的具體含 義。”
“我們早說過,抽象類是一種不能實例化而必須從中繼 承的類。抽象類可以完全實現,但更常見的是部分實現或者根本不實現,從而封 裝繼承類的通用功能,它可以提供已實現的成員,因此,可以用抽象類確保特定 數量的相同功能,但不能用接口這樣做。”
“接口是完全抽象 的成員集合,它的成員都無法在接口定義時實現,我們可以將它看作是為操作定 義合同,接口的實現完全留給開發者去做。它們之間的區別,如果認真分析,還 是有不少的:在VB.NET中,類只能是從一個基類繼承,所以如果要使用抽象類為 一組類提供多態性,這些類必須都是從那個類繼承的;接口就不一樣了,它不但 可以用一個類或結構實現多個接口,一個接口還可以有多個實現。 ”
“也就是說,它們在提供多態性的問題上是有差別的? ”我好象聽懂了點什麼。
“這是一個重要的區別,我們也可以 從多態性的角度來考慮是要使用接口還是用抽象類。”大李同意了我的觀點 ,“如果預計要創建組件的多個版本,我們應該創建抽象類。這是因為,抽 象類提供簡單易行的方法來控制組件版本。通過更新基類,所有繼承類都隨更改 自動更新。這是好處,當然也是問題,對吧?(詳見前文《脆弱的基類》)另一 方面,接口一旦創建就不能更改。如果需要接口的新版本,必須創建一個全新的 接口。所以,如果創建的功能將在大范圍的全異對象間使用,則使用接口。 ”
我想了一下,接著大李的話說:“能不能這樣說,抽象類主 要用於關系密切的對象,而接口最適合為不相關的類提供通用功能。 ”
大李對我伸出了大拇指:“不錯,小伙子悟性很好呀!你想 ,我上午跟你說,要創建控件,首先就是要對一些接口進行實現以讓系統能夠識 別(詳見前文《接口》)。而各個控件之間的聯系其實關聯性並不大。所以,它 們的基礎大都是接口。但是,我們要注意一點,在組件設計時,如果要在組件的 所有實現間提供通用的已實現功能,則使用抽象類。這是因為我們剛才說過的原 因,抽象類允許部分實現類,而接口不包含任何成員的實現。 ”
“唔,明白了,它們之間的區別有點明白了。”我默 默地點了點頭。
“另外,有個通用的設計思想,如果要設計小而簡 練的功能塊,則使用接口。如果要設計大的功能單元,則使用抽象類。”大 李又補充了一條建議。
“看來設計的問題還是蠻大的,一般來說, 怎麼設計接口呢?”我接著問。
“為什麼你所看到的編程書籍 也好,程序例程也好,極少有對接口的描述,而對類實現繼承的例子比比皆是? 這就從一個側面給我們提了一個醒,如果使用適當,接口可以成為很有用的工具 。但如果使用不當,它們會變得非常棘手,甚至妨礙有效的編程。接口的設計與 使用其實是一項高明的藝術。”大李鄭重其事的說。
“藝術? ”我驚叫了一聲。
“沒錯,藝術!”大李又加重了一下 語氣,“通過接口與實現的方式,我們可以將同一類型的程序運用在不同的 對象上面,而且不必修改原有類,相對子程序必須通過修改源程序代碼才能夠達 到重用的目的,接口與實現不僅是偉大的進步,也是境界極高的程序設計藝術。 ”
“哦,這倒是真的。”我回想起今天看到的接口的例 程。
“但是,最大的問題還是集中在接口設計上。”大李接著 說,“接口一旦被定義和接受,就必須保持不變,以保護為使用該接口而編 寫的應用程序。接口發布後,就不能對其進行更改。這是我們進行組件設計的一 個重要原則,叫做‘接口不變性’。”
我點了點頭: “接口不變性,這個我可以理解了。”
“我已經反反復 復強調過,創建一個接口就是在創建一個定義,接口定義發布後則永遠不能更改 。接口不變性,就是為了保護為使用接口而編寫的現有系統。當接口設計與需求 有所出入,確認需要大幅度變更時,我們應該創建新的接口。一般命名方式是, 新接口可以通過在原來的接口名稱後追加一個數字‘2’來命名,以顯 示出它與現有接口的關系。然後通過接口繼承來創建新接口。 ”
“可是,如果需求分析得不好,豈不是會出現一大堆的派生 接口了?”我不免有點顧慮。
“這是肯定的,而且過於頻繁地 生成新接口,會因未使用的接口實現而使組件變得很龐大。有經驗的設計師,在 充分分析需求後,設計出的接口往往很小且相互獨立,減少了性能問題發生的可 能。”
“這種分解能力倒真的是藝術呀!”我不禁為之 歎服。
“當然,一般來說,我們會把確定哪些屬性和方法屬於接口 的設計過程稱為‘接口分解’。基本准則是,應在一個接口中將緊密 相關的幾個功能聚合為一組。功能太多會使接口不便於運行,而過於細分功能將 導致額外的系統開銷並降低使用的簡易性。掌握分解的這個度的能力是需要不斷 的在技術上進行磨煉,以及在對每個項目深入分析與理解的基礎上才能得到的。 ”
“明白了。”我大聲地回答著,真希望自己能早一天 成為接口設計大師。
大李笑著拍了拍我:“明白了就好。其實,與 設計接口相比,創建大的實現繼承樹更容易出錯。”
“當然, ”我腦海裡浮現起實現繼承的各個環節,“這是不是說,在某些時候 適當使用接口還是很有益的。”
“看來你真的明白了,那你再 來說一下,接口與類實現繼承相比,好處有什麼?”大李回過身開始找茶杯 。
我低下頭,努力地轉動了一下腦子:“我試著說一下吧,總體而 言,接口是一種非常有效的編程方法,它讓對象的定義與實現分離,從而可以在 不破壞現有應用程序的情況下使對象得以完善與進化。接口消除了實現繼承的一 個大問題,就是在對設計實施後再對其進行更改時很可能對代碼產生破壞。即使 實現繼承允許類從基類繼承實現,在類首次發布時仍然會使我們不得不為設計做 很多的抉擇。如果原有的設想不正確,並非總可以在以後的版本中對代碼進行安 全的更改。比如,我們定義了一個基類的方法,它需要一個 Integer 參數,而後 來又確定該參數應該為 Long 數據類型。我們無法安全更改原始類,因為為從原 始類派生的類所設計的應用程序可能無法進行正確編譯。這一問題會擴大化,因 為單個基類會影響幾百個子類。”
“那用重載原始類並采用一 個Long類型的參數,不就能解決這個問題了嗎?”大李提了個問題。
“這個麼?”我想了一下,“可是,這樣不一定能達到 滿意的效果,因為一個派生類可能需要對采用整數的方法進行重寫,如果取 Long 數據類型的方法不能被重寫,該派生類可能無法正常運行。 ”
“那用接口怎麼做?”大李不依不撓地繼續問。
“辦法就是發布接受新數據類型的更新接口。”我一下子就回 答出來了。
“看來你已經掌握了接口操作的基本環節了。”大 李的評語真讓我高興。“我再幫你總結一下,使用接口繼承而不用類繼承的 主要原因有:在應用程序要求很多可能不相關的對象類型以提供某種功能的情況 下,用接口適用性會更強;接口比基類更靈活,因為可以定義單個實現來實現多 個接口;在無需從基類繼承實現的情況下,接口更好;在無法使用類繼承的情況 下接口是很有用的。例如,結構無法從類繼承,但它們可以實現接口。 ”
我抿著嘴用力點了點頭,同時在心裡默默地記憶著大李所說的每 一條准則。
“回去好好想想,多寫幾個小程序來練習一下,明天我 們還要欣賞VB.NET提供的強大的可視繼承的表現呢。”