多型算是比較三特性之中給人感覺比較溫和的了,不如說只要有了繼承,那麼多型的到來就是必然的。多型的定義是:不同的物件能夠做出一樣的行為,但必須由他們自己的程式碼來實作。

白話一點說就是:一樣的事,不同做法

多型相對是比較好理解的,畢竟每個人做同一件事的方法本來就不太一樣。例如一樣是泡奶茶,英國就正常地泡,美國就會用微波爐;一樣是肉粽,有些人就是比較愛吃油飯;到了程式的世界裡也是,即使繼承了同一個物件,實現這個行為的方式也可以不同。

在此可以先推菜鳥工程師肉豬的這篇 Java 什麼是多載(Overload), 覆寫(Override), 多型(Polymorphism) 中的說明。其中可以從例子看到儘管狗跟鳥都是繼承自動物這個類別,但對於「移動」這個方法,他們實作的方式並不一樣。這就是多型的範例。

我個人更喜歡用職位的方式去理解多型。

被繼承者就像是該職位的工作,例如說:Programmer 要會寫 C#、要會寫 SQL、要會 VB…等等,於是老闆就貼出了徵人啟示,要求新來的員工必須要有 Programmer 寫的能力:

public class Programmer
{
    public string WriteCSharp()
    {
        /* 努力地寫 C# */
    }
    public string WriteSQL()
    {
        /* 努力地寫 SQL */
    }
    public string WriteVB()
    {
        /* 努力地寫 VB */
    }
}

於是今天小華就來面試了,不過他寫的程式碼品質…不怎麼樣。

public class Hua : Programmer
{
    public string WriteCSharp()
    {
        return "ShitCode";
    }
    public string WriteSQL()
    {
        return "ShitCode";
    }
    public string WriteVB()
    {
        return "ShitCode";
    }
}

而小華應徵的隔天,小明也開開心心地來應徵了,他不只 C#、SQL 和 VB 都寫得很好,甚至還會泡英式奶茶:

public class Ming : Programmer
{
    public string WriteCSharp()
    {
        return "CleanCode";
    }
    public string WriteSQL()
    {
        return "CleanCode";
    }
    public string WriteVB()
    {
        return "CleanCode";
    }
    public Tea MakeTea() 
    { 
        return new Tea(teaName: "MilkTea"); 
    }
}

火速通過面試之後,老闆就讓小明上工了:

public void Work()
{
    Programmer programmer = new Ming();
    programmer.WriteCSharp(); // "CleanCode"
    programmer.WriteCSharp(); // "CleanCode"
    programmer.WriteCSharp(); // "CleanCode"
}

過了幾天後,老闆決定讓小明和小華一起寫同個專案:

public void newProject()
{
    Programmer programmer001 = new Ming();
    Programmer programmer002 = new Hua();

    programmer001.WriteCSharp(); // "CleanCode"
    programmer002.WriteCSharp(); // "ShitCode"

    programmer001.WriteCSharp(); // "CleanCode"
    programmer002.WriteCSharp(); // "ShitCode"
}

這裡也就是多型的核心概念,用子類別實作出各式各樣不同的方法,藉此讓父類別的方法藉此達到延伸和多樣化的效果。例如說一樣是 ProgrammerWriteCSharp() 這個方法,小明的實現就是 return "CleanCode"; 而小華的實作方式則是 return "ShitCode";

同樣地,最常被舉的例子就是動物。當有動物這個類別時,儘管狗跟貓都繼承了這個類別,但他們都可以對「叫聲」做出不同的實作。再藉由多型的方式,動物這個父類別,就能夠藉由子類別來完成擴展。更進一步來說,藉由子類別的擴展,我們能夠讓父類別做出各式各樣的事,而不需要更動父類別本身

最有可能碰到的例子就是資料庫連線。例如說,我們可以讓 MySqlDBConnectMongoDBConnect 都繼承 DBConnect,但各自保有對應不同資料庫的實作。如果一來,DBConnect 就獲得了用不同方法連線到不同資料庫的擴展,得到了對應狀況靈活使用的彈性。同時,使用 DBConnect 物件的其他物件也可以不用管現在的 DBConnect 連線是哪個子類別來工作的,只要知道能夠連線並取得資料就好,也達到了以封裝降低耦合的要求。

以上就是多型的核心,但當我們把子類別塞到父類別的殼裡面使用的時候,還需要注意以下這個部分:

當我們將小明宣告成工程師這一瞬間,小明已經不再只是小明,對老闆而言他就只是一個工程師,這裡只剩下「工程師」而不是「小明」,是 Programmer 而不是 Ming。在呼叫 Programmer programmer = new Ming() 的同時,這裡就只剩下一個無情的寫程式機器,再也沒有小明。

當然,小明也不准在上班時間泡英式奶茶,我們不是請你來做這個的!

public void Work()
{
    Programmer programmer = new Ming();
    programmer.WriteCSharp();
    programmer.MakeTea(); // Error: Programmer 未包含 MakeTea 的定義
}

這樣只會讓編譯器尷尬地說:請你照我們契約書上面走好嗎?畢竟這裡只剩下 Programmer 而不是 Ming 了,一個無情的寫程式機器是不需要,也不會知道怎麼泡英式奶茶的。

這邊我們就能知道:當子類別被以父類別的名義建立出來時,他就只能夠表現出父類別的樣子。換句話說,我們宣告的是什麼,他就只會做什麼,但要怎麼做倒是沒關係。雖然是一個令人悲傷的故事,但為了遵守封裝的精神,讓呼叫的物件不需要去了解是誰繼承、又由誰實作等等,為了物件界的秩序,這也是莫可奈何。

最後再說一聲,大話設計模式用兒子代替爸爸上台表演京劇的例子實在舉得很不錯,有興趣的可以去看看,這例子很能表現出那種披著父親的皮,用著自己的技術,但遮著臉不能被發現的感覺。如此傳神!


到這邊三大特性就說明完了,雖然我說明的相當模糊籠統,但希望概念能夠傳達到。

畢竟,就像我開頭引用的,我很喜歡這句「如果你問 100 個人這個問題,可能會得到 200 個答案,所以你一定要有自己獨到、有自信、精闢的見解或描述方式。」像我女友,當我問他物件導向的時候,她(大致上)是這樣說明的:

  • 封裝:醬包跟麵的工作都在泡麵工廠做完了,我們只要拿來泡就好
  • 繼承:我們可以買了泡麵之後,再自己加蛋加料
  • 多型:一樣是泡麵,實作出來的口味都不一樣

說得我都要去買一碗來煮了。但是,物件是為了貼近我們的現實世界,而每個人的世界觀本來就不一樣。你必須自己體會,然後才會有自己的觀點

至於那些說:你不是說要用卡牌當範例,怎麼突然多了個工程師小明小華?我只能說抱歉,洗澡的時候突然想到的,不寫不舒服。卡牌就想到再補吧,耶嘿。

下一篇開始就要進入抽象等等更模糊籠統的部分了,希望還能穩住哪。

本系列下一篇:菜雞與物件導向 (6): 抽象、覆寫

參考資料

推薦系列文

同系列文章