很多初學java的人都會有這樣的疑問:
我該用類別, 抽象類別還是介面?
尤其在學 java 之前沒有碰過物件導向概念的人。
在多種選擇的情況下,我該用哪一個比較好呢?
我們可以分幾個觀點來看這個問題
概念解釋觀點
在物件導向的語言裏面,物件跟類別是被抽象化的概念。
也得力於這樣的抽象化,我們可以把某些東西廣義化應用到不同的層面。
這樣的抽象化有賴於封裝、繼承、多型的落實。
封裝可以將概念相同的程式碼放在一起,這些程式碼做同一件事,所以他們代表同一個概念。
同時也比較好維護。
繼承可以提高 reusabiity 的機會,同時也展現了概念上的相似性。
多型就是一種同中求異的概念。
位於同一個繼承體系之下,可以有不同的行為。
承續我先前寫過的文章
繼承是is-a的概念,而實作介面是has-a的概念。
這些概念是不相違背的,所以我們可以簡單的把類別、抽象類別、介面分開。
類別跟抽象類別應該位於繼承體系之下,因為抽象類別也有is-a關係。
但是相對於(具體)類別,抽象類別是不允許被實體化(instantiate)的。
為什麼?
舉個例,Cat 跟 Dog 都繼承於 Animal,因為 Cat is an Animal and Dog is an Animal.
這時我們會把 Animal 寫成抽象類別,因為 Animal 被實體化之後不具任何意義。
他只是我們在繼承體系上或是概念上的類別,他並不是一個具體的物件,不具屬性跟行為。
所以 Animal 不應該被寫成(具體)類別。
那不能被實體化的類別我還需要去定義他的屬性跟方法嗎?
當然要!
回到我們的例子,通常只要是 Animal 就會 eat()
、move()
,或是我想給 Animal id
或是 name
。
這時我們不會把這些東西寫在 Cat 或是 Dog 裏面。
我們會把他通通寫到 Animal 裏面,如此一來下面的子類別就可以簡單的具有這些屬性跟方法了。
這時我們來講講介面,就如同我前面提到的。
介面的實作是has-a的關係。
所以他提供了在一個繼承體系之外的功能性。
他可以讓繼承於 Animal 的 Bird 擁有 fly()
的行為。
他可以讓繼承於 Animal 的 Turtle 擁有 swim()
的行為。
這些都是無法從 Animal 繼承過來的東西。
那無法繼承過來的話,自己寫就好阿,大驚小怪!
錯!
這時我們就會看到
會 fly()
(飛)的 Bird(鳥)。
會 transport()
(運輸)的 Airplane(飛機)。
會 swing()
(拍翅膀)的 Swan(天鵝)。
但是他們想表達的都是在天上飛的概念。
所以介面可以達到統一格式的效果。
大家都實作同樣的方法來表達同樣的行為。
或許行為的實際內容不一樣,但是在概念上是相同的。
軟體工程觀點
在前一個觀點當中提到抽象化。
抽象化使得同樣的一段程式碼可以提高他使用的頻率。
也達到**reusability (重複利用性)**的效果。
我們有了繼承體系來提高 reusability。
在程式碼上,子類別不用重寫同樣的程式碼,只要從父類別繼承來就好。
相對於同樣的程式碼散落在各個子類別,當要維護的時候就要一個一個修改。
這樣父類別就控制了子類別的行為。
如果父類別的方法被修改的話,也會影響到子類別。
如果你不希望父類別的改變影響到子類別的話,這時請確認兩者是否真的要使用繼承關係。
如果是繼承關係但又不希望受影響,那就用多型吧!
我們使用介面來提高系統的flexibility (靈活性)。
當你需要有相同概念的方法時只要實作同樣的介面就可以達到。
同時規定同樣的介面便於管理維護。
只要實作同樣的介面就可以簡單地擴充模組。
這是一個靈活的系統該有的特徵。
相對於多重繼承 (ex. C++)
相對於C++的多重繼承,java 只允許單一繼承。
在多重繼承當中,可以讓一個類別擁有很多身份。
一個類別可以是很多種東西。
這樣的概念其實可以讓人容易了解這個類別在做些什麼。
像是Computer is a Machine and Computer is a Calculator.
這樣我們可以理解電腦是一種會計算的機器。
但是相對應的他也帶來一些不必要的副作用。
繼承於多樣的父類別,也同時繼承到了父類別的屬性跟方法,但是並不是所有的屬性跟方法都是有用的,甚至有些屬性或方法根本用不到。
當你沒有要使用到的時候卻又繼承了太多的東西,這就造成了另一種的設計不良的狀況。
這會讓別人誤解子類別有擁有父類別的某些方法,但卻不知道那是不應該出現的。
造成誤用問題就更大了。
這樣的問題小至程式錯誤,大至系統崩潰,這就成了系統的弱點之一了。