深入比較介面與抽象類:從多個角度剖析

深入比較介面與抽象類:從多個角度剖析

Overview of Content

這篇文章不說明 interfaceabstract class 的基礎用法、功能,主要分析比較介面與抽象該如何看待、選擇

我們將從多個角度深入比較介面與抽象類的差異

我們將從 隱定性、可變性、複雜度、多實現、可維護性、重構性和耦合性 等多個方面進行分析比較… 無論您是新手還是有經驗的開發者,本文都將為您提供深入且全面的見解,幫助您更好地理解和選擇介面和抽象類的適用情境

寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀


介面 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 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!幫助你針對物件導向設計有更深入的了解!


Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響