Overload (多載) vs. Override (覆寫) — (II)

發表於 分類為「設計模式/原則

 
上篇 中,介紹了 Overload (多載) 的種類及實作技巧,

接下來則要討論 — Override (覆寫)
 
儘管強調兩者的差異,它們仍息息相關 🤔,

皆是實踐 多型 (polymorphism) 的技術之一,

善用這些技巧,才能有效實作彈性、可擴充的程式!
 
在開始之前,強烈建議閱讀 依賴倒置原則 (DIP)

您將能對 多型、抽象、介面…有進一步的認識 😁。
 
overload-vs-override
 
[註]:
若無指明,本篇使用的語言預設為 Java。

 


 

如果沒有 Override (覆寫)

 
先別管 Override (覆寫) 了,你聽過 吱吱喳 嗎?


 
這是一隻會發出吱吱叫的 小鼠 🐿 (父類別):

class Mouse {

    // 省略成員變數 (一些特徵如 眼睛、鼻子、牙齒...)

    // call (V.) 動物叫
    void call() {
        System.out.println("吱吱喳");
    }
}

 
有天,誕生了一隻基因突變的怪老鼠,稱為 皮卡丘 (子類別),
他不再吱吱叫,而是發出『皮卡~ 皮卡~』:

class Pikachu extends Mouse {

    // 保有父類別的成員變數

    void pika() {
        System.out.println("皮卡~皮卡~");
    }
}

 

 
 

型別比較運算子 (instanceof)

無聊的我 🕺,為了比較不同 Mouse 間的叫聲,

在高階模組 Main 中,撰寫一方法 testMouse(Mouse mouse)
 
實作方式很簡單!使用 型別比較運算子 — instanceof

即可得知傳遞進來的『 mouse 型別 』,究竟是否為 皮卡丘

static void testMouse(Mouse mouse) {

    if (mouse instanceof Pikachu) {
        ((Pikachu) mouse).pika(); // 是皮卡丘,則 轉型並呼叫 pika() 方法
    } else {
        mouse.call(); // 不是皮卡丘,則 直接呼叫 call() 方法
    }
}

 
實際 Run Run 看:

public class Main {

    public static void main(String[] args) {

        Pikachu 皮卡丘 = new Pikachu();

        testMouse(皮卡丘); // 將 皮卡丘 丟進實驗室
    }
}
Output: 皮卡~皮卡~

 
大成功 😇!
 
Q:
testMouse(Mouse mouse) 方法要求的「參數型別」明明是 Mouse

為何能將 Pikachu 傳進去?
 
Ans:
透過 多型 (polymorphism) 機制,

我們能以 父類別/介面 操作其 子類別;反之則無法。

Pikachu 繼承自 Mouse

 
 

if-else 長鍊

某天,又演化出一種 頭頂局部燙傷 的老鼠,稱為 哈姆太郎 (Mouse的子類別):


 
他的特色是:一興奮時,就會發出『 Heke 』的聲響

class Hamutaro extends Mouse {

    void excite() {
        System.out.println("Heke~");
    }
}

 
別忘了!由於我們擴充了 子類別

高階模組testMouse(Mouse mouse)被迫 必須修改 :

static void testMouse(Mouse mouse) {

    if (mouse instanceof Pikachu) {
        ((Pikachu) mouse).pika();
    } else if (mouse instanceof Hamutaro) {
        ((Hamutaro) mouse).excite(); // 是哈姆太郎,則 轉型並呼叫 excite() 方法
    } else {
        mouse.call();
    }
}

 
實際 Run Run 看:

public class Main {

    public static void main(String[] args) {

        Hamutaro 哈姆太郎 = new Hamutaro();

        testMouse(哈姆太郎);
    }
}
Output: Heke~

 
大成功 😇!……….嗎?
 
您是否發現 目前的做法:

每擴充一種 子類別,高階模組 的 testMouse()方法,就被迫跟著改變

試想一下,若繼續擴充 米老鼠、傑利鼠、雷丘、拉達、地鼠、土撥鼠…,

您的 testMouse()方法 將長成這副德性:

static void testMouse(Mouse mouse) {

    if (mouse instanceof Pikachu) {
        ((Pikachu) mouse).pika();
    } else if (mouse instanceof Hamutaro) {
        ((Hamutaro) mouse).excite();
    } else if (mouse instanceof Mickey) {
        ((Mickey) mouse).xxxx();
    } else if (mouse instanceof Jerry) {
        ((Jerry) mouse).yyyy();
    } else if (mouse instanceof Raichu) {
        ((Raichu) mouse).zzzz();
    } else {
        mouse.call();
    }
}

 
這就是聲名狼藉的 if-else 長鍊

因此,這似乎不是什麼好的擴充方式 🤔…。

 
 

Liskov 替代原則

對不起,我講的太過委婉了,

上述 所有的程式碼,不只不是好方式,根本就 😂:

都是垃圾
 
 
你可能會覺得:擴充 不過就加個 if-else 而已,有這麼誇張嗎?
 
Ans:
絕對有 😂。

若只有 Main 類別使用到 Mouse 倒還好,

但若有 多個類別 或 多個方法 使用到低階模組 Mouse,不是改到死 就是 漏東漏西 🙀。
 
而之所以造成上述的種種問題,一切皆因 違反 Liskov 替代原則

 
 

何謂 子類別 (subclass)?

Liskov 替代原則 告訴我們:

子型別 必須可以 替換 (substitute) 他們的父型別。

 
要讓 父類別 Mouse 發出聲音,可以透過 call() 方法,

但要讓 子類別 Pikachu 發出聲音,卻得修改 成使用 pika() 方法。

『 卻得修改 』便是 — — 不可替換

[註]:
雖然 Pikachu 一樣能使用 call() 方法,但 結果 (後置條件) 並非預期
除非你想要只會吱吱喳的皮卡丘 😂。
 
 
於是,一個非常非常非常重要的結論:

子型別 真正的定義是 — — 可替換的 (substitutable)

 
軟體開發大師 Robert C. Martin

正是因為 子型別的 可替換性,讓以基礎型別表達的模組得以 不需修改而加以擴充

 


 

Override (覆寫)

 
大師 Kent Beck 告訴我們:

子類別 傳遞的資訊應該是『 我 和 超(父)類別 很像,只有 少許差異 』。

Pikachu 繼承了 Mouse,因此能共享其部分的結構或行為:

1. 成員變數 (一些特徵如 眼睛、鼻子、牙齒…)
2. 方法 (call、eat、drink)
 
然而,必須 挑出其中的 少許差異 並加以修改

才能使之成為 可替換的 (substitutable) 子類別。
 
Override (覆寫) 便是 修改那些差異 的主要機制之一 😎:

Override (覆寫) 讓 子類別 能以異於 父類別 的方式處理訊息。

 
當然,就像跟朋友借作業抄之前,你得要有朋友


 

子類別 要能 Override (覆寫),前提是有 父類別/介面

[註]:
Overload (多載),則不需要。

 
 

重新定義

Override (覆寫),又譯為:重寫、改寫,

有「 撤銷,推翻;使無效 」之意。
 
簡而言之,就是:

重新定義

用於:

使 子類別 覆寫 (重新定義)繼承/實作自 父類別/介面 的方法 』。

 
於是,子類別 (Pikachu) 只需 覆寫 (重新定義)繼承自 父類別 (Mouse) 的 方法 (call) 』,

便能同樣透過 call() 方法,達成預期的結果!

class Pikachu extends Mouse {

    @Override
    void call() {

        System.out.println("皮卡~皮卡~");
    }
}

 
此時 Pikachu 的 call() 方法已經 重新定義

若執行該方法,輸出結果將為『 皮卡~皮卡~ 』,而非 繼承自父類別的『 吱吱喳 』:

public class Main {

    public static void main(String[] args) {

        Pikachu 皮卡丘 = new Pikachu();

        皮卡丘.call();
    }
}
Output: 皮卡~皮卡~

 

 
 
[註1]:
上例的 標註 (Annotations) — @Override,只是一個好習慣,而非規定!
告知編譯器,此方法試圖 覆寫 父類別方法,
並幫助開發時釐清 方法 的類別層級。
 
[註2]:
當初為何翻譯為 覆『寫』,我也相當好奇 😂,
可能是 ride 跟 write 唸法 87% 像 🤔?
又或者只是一種 意譯 (paraphrase)

 
 

再見了,if-else

由於確保了 子類別 的行為與 父類別一致,

高階模組 Main 中的 testMouse(Mouse mouse) 方法,

不必 再使用 型別比較運算子 — instanceof 😇!

public class Main {

    public static void main(String[] args) {

        Pikachu 皮卡丘 = new Pikachu();

        testMouse(皮卡丘);
    }

    static void testMouse(Mouse mouse) {
        mouse.call();
    }
}
Output: 皮卡~皮卡~

 
 
Q:
若保持此原則,繼續擴充 米老鼠、傑利鼠、雷丘、拉達、地鼠、土撥鼠…?
 
Ans:
testMouse(Mouse mouse) 永遠就是長這樣唷 😉:

static void testMouse(Mouse mouse) {
    mouse.call();
}

 
因此可知,若所有子類別皆具備 可替換性 (顯式 or 隱含的符合父類別之方法契約),

結果就是 高階模組的 再利用 變得相當簡單 😇。

再見了,instanceof

再見了,冗餘的轉型

再見了,低能的 if-else


 
[註]:
若您的程式碼仍充滿了 if-else + instanceof
往往代表未能善用 多型 及 沒有優良的繼承體系。

 
 

方法簽章 (Method-Signature)

在上一篇中,提及了 方法簽章 (Method Signature) 的觀念:

方法簽章 (Method Signature) = 方法名稱 (method’s name) + 參數型別 (parameter types),

用以決定方法的 唯一性,其中 Signature 又譯為:外貌簽名、簽署、署名。

並不包含 回傳型別 (return type)。

 
同上述範例,Override (覆寫) 的使用方式就是:

子類別 定義一個『 與 父類別/介面 相同 方法簽章 』的方法。

例如,這是一個標準的 Override (覆寫) 範例,

其中,子類別 SubClass 與 父類別 SuperClass 的 earn(int i) 方法:

方法簽章 (方法名稱 + 參數型別) 完全相同

public class Main {

    public static void main(String[] args) {

        new SubClass().earn(500);
    }
}

class SuperClass {

    void earn(int i) {
        System.out.println("老爸賺了: " + i);
    }
}

class SubClass extends SuperClass {

    @Override
    void earn(int i) { // 方法名稱 與 參數型別 同父類別
        System.out.println("兒子賺了: " + i);
    }
}
Output: 
兒子賺了: 500

 
 
如果沒有 覆寫 呢?

public class Main {

    public static void main(String[] args) {

        new SubClass().earn(500);
    }
}

class SuperClass {

    void earn(int i) {
        System.out.println("老爸賺了: " + i);
    }
}

class SubClass extends SuperClass {
    // 未覆寫方法
}
Output: 
老爸賺了: 500

 
似乎哪裡怪怪的 🤔?

 
 

爸,我 super 對不起你

上方的範例,有一個 很大的問題

不是兒子賺錢,就是老爸賺錢 😂!

 
雖然,有些父親希望兒子爭氣,甚至會因 ↓ 感到失望:

Photo by Movie Inception.
 
 
然而:

對不起了 爸,請您把錢給我 😂。

要取得老爸一切成就的鑰匙 🔑,就是 — — super 關鍵字 😈,

當然,前提是 父類別 該方法存取等級 並非 private:

public class Main {

    public static void main(String[] args) {

        new SubClass().earn(500);
    }
}

class SuperClass {

    void earn(int i) {
        System.out.println("老爸賺了: " + i);
    }
}

class SubClass extends SuperClass {

    @Override
    void earn(int i) {
        super.earn(100000); // 呼叫父類別方法    
        System.out.println("兒子賺了: " + i);
    }


    // 不需要是 覆寫方法 (Overriding Method) 也行:
    void test() {
        super.earn(1000); // 呼叫父類別方法
    }
}
Output:
老爸賺了: 100000
兒子賺了: 500

 
 

感恩老爸,讚嘆老爸 🙇

 
由此可知:

Override (覆寫) 除了能 重新定義 父類別方法,

還能透過 super 關鍵字,以原先的方法為基礎,加以 擴充

 


 

總結

 
還記得文初所述嗎:

透過 多型 (polymorphism) 機制,

我們能以 父類別/介面 操作其 子類別;反之則無法。
 
事實上,Override (覆寫) 的好處,務必配合 多型 才能有效發揮,

否則,可說是 沒有任何意義
 
因此,目前的程式碼依舊 😂:
都是垃圾
 
 
礙於篇幅,本篇僅闡述 覆寫 解決的問題、功用 及 效益,

實戰 與 多型 的用法就下篇再談囉 😃。
 
 
 
 



作者: 鄭中勝
喜愛音樂,但不知為何總在打程式😱
期許能重新審視、整理自身所學,幫助有需要的人。

在《Overload (多載) vs. Override (覆寫) — (II)》中有 4 則留言

發表迴響