關於在Interface和Abstract Class間選擇的一些思考

Senolytics發表於2024-05-04

本文系筆者在學習軟體構造課程期間所寫,不保證通用性和正確性,僅供參考。
基於課程要求,本文所涉及語言為Java。

目錄

  1. 前言
  2. 介面:元件思想
  3. "Composition over Inheritance"
  4. 何時選擇繼承類
  5. 結語

一、前言與簡要介紹

在學習軟體構造課程之前,自己寫程式碼遇到需要複用類中功能時,基本上都是優先選擇繼承類的,而介面對我來說只有“新增某個小功能”的作用,比如在一個“電腦”的類中加入"USB插口"的功能。

然而在課程中,對實現一個有向圖這樣比較“大”的功能也選擇了用介面來作為“基類”,這與我之前的思維有所衝突,於是決定研究一下介面與基類到底有什麼差別,什麼時候用誰比較好。

那麼以下是對二者異同的介紹(因為重點不在這裡,所以只做簡要介紹,並不全面和嚴謹,具體請參考官方文件):

共同點:

  • 都不能被直接例項化;
  • 都可以在其中定義抽象方法;
  • 都可以定義有具體實現的方法(JDK1.8以後),不過程式碼中的體現方式不一樣。
  • 都可以定義靜態方法與變數。(注意關鍵字static與default、abstract衝突)
  • 都可以定義變數。

不同點:

  • 在Java中,介面可以多繼承,而抽象類只能單繼承。
  • 方法上:介面中的方法預設都是抽象方法,預設不能有方法體,如果想要有方法體,則需要在方法前加上default修飾(並且只有介面中才有這種修飾)。介面中的方法只能是public。而抽象類中的方法預設與普通類的方法沒什麼不同,只有在不需要寫實現的時候宣告一下abstract。可以看到,某種意義上二者在抽象方法上的思路是正好相反的。
  • 變數上:介面中的變數預設都是public static final,而且只能是public,必須在宣告時進行賦值。這裡的思想大概是定義一個常量,但是如果宣告的是可變型別的話實際上就沒什麼意義了(所以一般也不會在介面中大量宣告變數)。而抽象類中的變數同樣與普通類沒什麼不同。

二、介面:元件思想

說回來,介面的思想當然不只是新增一個小功能,那麼它體現的思想是什麼呢?我認為主要有以下兩點:

  1. 作為一系列類的大規約,相當於宣告瞭API。即一個類只要實現了這個介面定義的這些方法,那麼理論上它就可以勝任這個介面所期待可完成的任務。這樣的好處一是可以一眼就明白所有繼承介面的類都有什麼基本功能,二是一個類可以實現多個這樣的介面,從而扮演多個角色。
  2. 作為元件存在。這一點就相當於之前提到的“新增小功能”了。有的介面可以不必具有一套完整的功能,而只是相當於更通用的元件,或者說外掛一般,插在一個類上,它就可以做這樣一些功能。

事實上,介面反映的這種元件思想在程式碼中的應用相當廣泛。舉我最近在學習的遊戲引擎為例,為遊戲物體新增功能也是一種典型的元件思想。

例如,在Unity中,透過新增元件(Component)為遊戲物體新增功能:

Unity

如上圖中的這個物體,就同時具有Rect Transform、Animator和自己編寫的BottomInfo這三個元件所賦予的功能。

在最近比較火的Godot中,更是發揮了元件思想的特點,對元件(指令碼)進行了視覺化的介面設計,將指令碼本身也拆分成了視覺化的元件。(我還沒用過呢就不多說了免得出錯x

圖片來自Godot官方文件

從某種意義上來說,函式庫的抽象和封裝也可以看作元件思想的一種體現,在程式碼中引入了某個庫,就相當於給它裝上了這個元件,它就具備了這個元件所提供的功能。

說到這倒不如反過來:介面的元件思想其實是這種更大層面上的“元件思想”在程式碼層面上的一種體現,只不過介面本身一般情況下並不具備真正的功能,而只是定義了該有的功能而已。

三、"Composition over Inheritance"

於是便不得不談到一句老話:“Composition over Inheritance.”就是說,總是偏向於優先選擇元件而非繼承。為什麼呢?下面舉一個案例來體現繼承中可能遇到的問題,以及介面如何巧妙地化解這一問題。

現在,假設你手頭有一個GUI庫,正在構建一個GUI介面。你想向介面中新增一個按鈕,這需要用到庫中已經封裝好的Button類。不僅如此,該庫還提供了豐富的按鈕變種,比如MenuButton(選單按鈕)、SilderButton、DropdownButton等。現在甲方希望在選單中新增一個按鈕,它有個獨特的需要你自己寫的功能,比如按下之後有特效。

如果使用繼承的話,直觀的思路就是新開一個類繼承MenuButton,比如命名為SpecialMenuButton吧。

public class SpecialMenuButton extends MenuButton{
    public void SpecialEffectOnClick(){...}
}

它很好地完成了它的使命,沒啥毛病。

現在甲方希望所有的按鈕都要有這種特效。那思路似乎也挺清晰的,新開一個類直接繼承Button類就好了嘛。

public class SpecialButton extends Button{
    public void SpecialEffectOnClick(){...}
}

可是問題來了,如果之後甲方希望SliderButton、DropdownButton也有這種特效,那就用不了SpecialButton了,因為它並不具備Slider和Dropdown的功能。
如果繼承這兩個類,因為SpecialButton跟它們又沒有什麼關係,那又得重新寫一遍產生特效的方法,久而久之,重複的程式碼會越來越多,各種功能排列組合的類也越來越多,整個程式碼就會變得越來越冗餘。

繼承的思想這時就不好用了。那麼看看介面如何解決:

public interface HaveSpecialEffect{
    // void SpecialEffectOnClick();
    default void SpecialEffectOnClick(){...}
}

之後想要一個按鈕有特效只要實現這個介面就行了,甚至如果介面裡已經寫好了預設方法體,那麼就真正成為了一個即插即用的元件了。

其實對於上面這個問題,解決的方法有很多,純介面也不是最好的解決方法,其他的解決方法有定義一個方法,它傳入一個按鈕類,並控制按鈕產生特效;或者使用如Decorator等的設計模式。總之還是跳出了繼承的思想,從而避免了無用的重複。

四、何時選擇繼承類

既然如此,那麼抽象類還有什麼存在的意義呢?什麼時候才會選擇繼承一個抽象類?

一是在寫具體實現的時候。Java中,現在介面雖然可以定義預設的方法體,但本質思想也只是起一個規約的作用。如果類裡有複雜的方法,比如一個public的方法得呼叫好幾個private的方法,那用介面寫起來就比較難受了。此外,介面裡定義的變數都是static的,也不便於寫複雜的實現。
這時,在抽象類裡寫好實現,選擇性地留一點小的抽象方法留給繼承類發揮空間,就很方便了。這裡的抽象類更多地已經可以成為一個普通的類,只不過不希望它被例項化而已。

二是在框架結構很清晰的時候。上文提到的按鈕的例子,未來的要求還有很多不確定性,但是如果已經可以預見未來在框架上不會有大的變動時,一些抽象類可以一定程度上簡化程式碼佈局:通用的方法就可以直接在抽象類裡寫完了。

一句話,介面更偏向抽象,抽象類更偏向實現。

五、結語

總結下來,我的感受是其實介面和抽象類的區別更多是在思想上,而非語言的定義上,比如在C++中類也可以多繼承,介面和抽象類的差異就更小了。

除了選擇好介面和抽象類之外,它們各自或二者結合還可以衍生出很多設計模式,這些設計模式又可以填補單用介面或單用繼承的不足,因此更加體現了這種介面和抽象類間的比較更多是在思想上而非具體實踐中。

這篇文章其實早該發了,但是一是懶二是覺得該儘量寫的正確性實用性高一點,所以一直拖到課都上完了才發......然而寫完並不自覺有多好,相反,感覺侷限性挺大,而且思路比較跳躍。作為課堂任務來說算是有自己的思考了;作為一篇正經的博文來說,不敢說有什麼用,如果能幫到你那真是再好不過了。

相關文章