介面 vs 抽象:從不同程面比較
繼承類別(abstract class
)、介面(interface
)都位於物件的繼承樹上層,都無法被實體化,但兩者有哪裡不一樣嘛?這就是這個小節要探討的,我們以不同的角度來看看者兩種抽象化的差異點…
比較角度:隱定性、可變性
● 介面由如合約:它對外公佈給使用方,這會導致它有一個特性,介面必須穩定,不可任意更改(不管是增加、刪除)合約,否則會影響到所有實做的子類
這裡我們不考慮 interface 的
default
方法
interface FileReader {
// 新增方法
boolean isEmpty();
int read(byte[] buffer);
}
// 子類必須實做 isEmpty 否則會導致編譯不過
class DefaultFileReader implements FileReader {
@Override
public int read(byte[] buffer) {
// TODO:
}
}
● 介面隔離原則(想了解更多請點進連結)
所以在設計介面時必須要嚴加小心思考,進而產生了另一種設計界面的概念,介面隔離(介面最小化) 來讓介面更有可控性,降低使用代價
● 抽象具有可調性:而相對比起來,抽象在更改時不一定會影響到子類(新增共有方法,也就是預設實做),相較介面,它更具有可調整性
// 子類必須實做 isEmpty 否則會導致編譯不過
abstract class CommonFileReader {
// 新增共有實做方法,子類不一定需要實現
protect boolean isValid(byte[] buffer) {
// TODO:
}
}
class UTF8FileReader extends CommonFileReader {
}
class UnicodeFilreReader extends CommonFileReader {
}
比較角度:複雜度、多實現
● 介面可以多實現、降低複雜度:
Java 的特性之一就是類只能單一繼承,而 介面(interface
)的實做可以有多個;
而以 JVM 的角度來看介面的方法,介面方法由有實做類來完成該方法,不會有複寫的狀況,降低了 JVM 動態連結的複雜度(對於效能的提昇)
interface IShowMessage {
void showMessage(String str);
}
interface ISettingMessage {
void setMessage(String str);
}
// 可實做多個界面
class DefaultShowMessage implements IShowMessage, ISettingMessage {
@Override
public void showMessage(String str) {
// TODO
}
@Override
public void setMessage(String str) {
// TODO
}
}
● Java 為何不設計多繼承
像是
C++
、Dart
... 等等語言的多繼承,在處理類的聯繫規則時會變得複雜,而 Java 語言的設計理念則是降低語言的複雜度所以也有人說
Java
其實是C++--
,也就時 降低 C++ 的複雜度,只提高它的普片通用性
比較角度:可維護性、重構性
● 可維護性、重構性:從已經有的(已設計好的)類別中,要再進行抽象並不容易,因為你只要抽象化所有的方法,其繼承的子類都必須實做新的方法
這裡並不考慮在
abstract class
中的實做方法,目前只考慮「純抽象」
// 已經設計好的類
abstract class Firmware {
// 要拓展(重構)的新方法,子類沒有實做是不行的
abstract boolean update();
abstract String version();
}
class HMI extends Firmware {
@Override
String version() {
return "1.1.1";
}
}
class LED extends Firmware {
@Override
String version() {
return "0.3.8";
}
}
● 那要換做界面(interface
)會比較好嗎?
這又會牽扯到前面說明的概念,基於 類可實現多介面 所以我們可以透過設計界面來達成重構時的新抽象,這可以達到兩個好處
A. 不必對已有的抽象類新增抽象
// 新功能使用介面拓展
interface IUpdate {
boolean update();
}
// 原有的類保持
abstract class Firmware {
abstract String version();
}
B. 只需要對需要的類進行界面的實做(繼承無法達到,因為 Java 不許多繼承),不用依賴多餘界面
// 針對需要的類進行實做即可
class HMI extends Firmware implements IUpdate {
@Override
String version() {
return "1.1.1";
}
@Override
public boolean update() {
// TODO
return false;
}
}
// 不需要的類不實做介面
class LED extends Firmware {
@Override
String version() {
return "0.3.8";
}
}
● 要如何為 介面取名?
A. 首先我們要知道「取名」的重要性,它有關到之後程式的可讀性、可維護性
B. 在語法上,介面不會強迫它的實做必須與類名的實做在語意上有強烈關係,介面名只須跟須拓展的類有關連性即可
也就是說我們在為界面取名時,要取與該界面最相關的名稱,而不是與類最相關的名稱!
比較角度:耦合性
● 如果要選擇一個作為 方法宣告,選擇介面更為合適,原因如下
A. 介面(interface
)更加單純,不會參雜多餘的邏輯實做
抽象如果要過展就必須不斷的繼承下去,有很多時候會「被迫」拓展出不須的功能;相對起來抽象更屬於實做 & 介面之間的產物
我們看看以下範例:這個範例中闡述的是抽象類繼承的「強迫性」
abstract class Fan {
abstract void turnOnFan();
abstract void freshAir();
}
class NormalFan extends Fan {
@Override
void turnOnFan() {
System.out.println("Start Fan~");
}
// 強迫繼承了不需要的功能抽象,只能使用拋出
@Override
void freshAir() {
throw new UnsupportedOperationException();
}
}
class LGAirPurifier extends Fan {
@Override
void turnOnFan() {
System.out.println("Start LG Fan~");
}
@Override
void freshAir() {
System.out.println("Start LG fresh air~");
}
}
B. 界面語法上的支援限制更少:
一個類可以實做多個介面,也就是說它可以完成多個功能,並不像抽象類會受到限制
interface IFan {
void turnOnFan();
}
interface IFreshAir {
void freshAir();
}
class NormalFan2 implements IFan {
@Override
public void turnOnFan() {
System.out.println("Start Fan~");
}
}
class LGAirPurifier2 implements IFan, IFreshAir {
@Override
public void turnOnFan() {
System.out.println("Start LG Fan~");
}
@Override
public void freshAir() {
System.out.println("Start LG fresh air~");
}
}
介面、抽象:選擇考量
在上面我們用不同的角度來比較界面跟抽象,似乎界面看起來更好一點?
不!它們各有各的特點,應該各司其職,在正確的時機使用它們才是重點,那接著我們就來看看,要如何選擇考量…
介面:對外服務
● 界面(interface
)對外提供服務:
抽象有分「層」,最高層的抽象就是界面,介面應該作為與外界溝通的視窗,並且它傳達出的是一種 契約規範(依照規則就可以達到像對應的處理)
不論大小系統之間,都應該使用界面進行互動,這樣可以有效的提高鬆耦合
注意:界面需要謹慎思考,因為 開出的介面不可任意修改,也請記得善用 介面隔離原則
● 我們看看以下範例,該如何使用界面對外提供功能,並降低類與類之間的耦合度:
A. PowerManager
子系統:
對外提供 IPowerManager
介面讓其他子系統使用,也就是對外提供給使用者時,是對外提供 IPowerManager
介面,不讓使用者關注內部實做,進而達到 鬆耦合 的特色(讓使用者依賴在界面合約上,而不是實做之上!)
/*
* public 層級
*/
public interface IPowerManager {
void lowPower();
void typicalPower();
}
/**
* package 層級
*/
class DefaultPowerManager implements IPowerManager {
@Override
public void lowPower() {
System.out.println("Low power mode.");
}
@Override
public void typicalPower() {
System.out.println("Typical power mode.");
}
}
B. WindowManager
子系統
● IWindow
界面用來顯示、關閉視窗:對外部暴露界面(IWindow
),而實做 DefaultManager
透過 package 層級來對外隱藏!
// 對內部提供功能,也可以用契約,但是不會像外部這麼嚴厲
/*
* public 層級
*/
public interface IWindow {
void showWindow();
void closeWindow();
}
/**
* package 層級
*/
class DefaultManager implements IWindow {
@Override
public void showWindow() {
System.out.println("Show window.");
}
@Override
public void closeWindow() {
System.out.println("Close window.");
}
}
● IWindowManager
界面內部依賴抽象 IWindow
界面來達到隱藏實做細節類的目的,同時對外也只提供 IWindowManager
界面,對實做類同樣使用 package 層級來隱藏
並且我們可以看到,在這樣的設計之下也隱藏了
DefaultWindowManager
依賴IPowerManager
界面的細節!
// 對外的保證契約
/*
* public 層級
*/
public interface IWindowManager {
void addWindow(IWindow window);
void removeWindow(IWindow window);
}
/**
* package 層級
*/
class DefaultWindowManager implements IWindowManager {
private final List<IWindow> windowList = new ArrayList<>();
private final IPowerManager powerManager;
public DefaultWindowManager(IPowerManager powerManager) {
this.powerManager = powerManager;
}
@Override
public void addWindow(IWindow window) {
if(!windowList.contains(window)) {
windowList.add(window);
}
if (windowList.size() > 100) {
powerManager.lowPower();
} else {
powerManager.typicalPower();
}
window.showWindow();
}
@Override
public void removeWindow(IWindow window) {
windowList.remove(window);
window.closeWindow();
}
}
● 對外提供給使用者:可以看到我們可以對外提供給使用者純界面的類型,而不需要對外暴露實做!
public SystemWindow {
public static final SystemWindow instance = new SystemWindow();
private SystemWindow() { }
// 對外提供界面
private final IPowerManager powerManager = new DefaultPowerManager();
// 對外提供界面
public final IWindowManager windowManager = new DefaultWindowManager(IPowerManager);
}
抽象類:對內擴充
● 抽象類(abstract class
)繼承對內擴充:
抽象屬於介面、實做之間的 半成品,抽象除去各個子類的特點,它會盡可能的完成共通邏輯的實做;盡量使用在系統內部使用
當一個方法是為了特定的擴充(該擴充是給內部系統呼叫),而不是為了對外服務(外部呼叫該擴充則無用),那該類就可以使用抽象!
● 對內擴充的範例:
A. 主系統如下:透過介面來說明對外的承諾,這次在其中我們添加了一個 VerifyRule
類,用來給使用者擴充驗證方案
interface IVerifyManager {
boolean verify(String str, VerifyRule rule);
}
class DefaultVerifyManager implements IVerifyManager {
@Override
public boolean verify(String str, VerifyRule rule) {
if (str == null || str.isEmpty()) {
return false;
}
// Maybe do something...
return rule.startCheck(str);
}
}
// 待驗證項目
abstract class VerifyRule {
protected VerifyRule next;
// 可拓展的方法
protected abstract boolean check(String str);
// 對內統一方法
final boolean startCheck(String str) {
boolean currentCheck = this.check(str);
if (!currentCheck) {
return false;
}
if (this.next != null) {
return this.next.startCheck(str);
}
return true;
}
}
● 這裡記得,非對外提供擴充的方法要用
final
描述,意在於不讓外部類覆寫,保持類設計的安全性!
B. 使用者拓展、擴充:這個擴充的要點是,它會讓內部系統經過邏輯驗證後,再去呼叫,使用者不會知道該擴充被呼叫的時機點
class JWTRule extends VerifyRule {
public JWTRule() {
this.next = new CRCRule();
}
@Override
protected boolean check(String str) {
return str.contains("JWT");
}
}
class CRCRule extends VerifyRule {
@Override
protected boolean check(String str) {
return str.contains("CRC");
}
}
更多的 Java 語言相關文章
Java 語言深入
● 在這個系列中,我們全方位地探討了 Java 語言的各個核心主題,旨在幫助你徹底掌握這門強大的編程語言。無論你是想深入理解 Java 的基礎類型與變數作用域,還是探索異常處理與運算子的細節,這些文章都將為您提供寶貴的知識
此外,我們還涵蓋了物件創建、函數式編程、註解應用以及泛型的深入分析,幫助您提升在實際開發中的技能和效率
點擊以下連結,開始你的學習之旅~
● 深入探索 Java 基礎類型、編碼、浮點數、參考類型和變數作用域 | 探討細節
● 深入了解 Java 應用與編譯:從原始檔到命令產出 JavaDoc 文件 | JDK 結構
● 深入理解 Java 運算子與修飾符 | 重要概念、細節 | equals 比較
● 深入探索 Java 物件創建與引用細節:Clone 和finalize 特性,以及強、軟、弱、虛引用
● 認識 Java 函數式編程:從 Lambda 表達式到方法引用 | 3 種方法引用
Java IO 相關文章
● 探索 Java IO 的奧秘,了解檔案操作、流處理、NIO等精彩內容!
深入 Java 多執行緒
● 這一系列文章將帶你深入了解 Java 多執行緒技術的各個方面,從基礎知識到進階應用,涵蓋了多執行緒編程的核心概念與實踐
深入 Java 物件導向
● 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!幫助你針對物件導向設計有更深入的了解!