【設計模式】如何用組合替代繼承

丹楓無跡發表於2020-06-15

如果問物件導向的三大特性是什麼,多數人都能回答出來:封裝、繼承、多型。

繼承 作為三大特性之一,近來卻越來越不推薦使用,更有極端的語言,直接語法中就不支援繼承,例如 Go。這又是為什麼呢?

為什麼不推薦使用繼承?

假設我們要設計一個關於鳥的類。

我們將“鳥類”定義為一個抽象類 AbstractBird。所有更細分的鳥,比如麻雀、鴿子、烏鴉等,都繼承這個抽象類。

大部分鳥都會飛,那我們可不可以在 AbstractBird 抽象類中,定義一個 Fly() 方法呢?

答案是否定的。儘管大部分鳥都會飛,但也有特例,比如鴕鳥就不會飛。鴕鳥繼承具有 Fly() 方法的父類,那鴕鳥就具有“飛”這樣的行為,這顯然不符合我們對現實世界中事物的認識。

解決方案一

在鴕鳥這個子類中重寫 Fly() 方法,讓它丟擲異常。

public class AbstractBird
{
    public virtual void Fly()
    {
        Console.WriteLine("I'm flying.");
    }
}

//鴕鳥
public class Ostrich : AbstractBird
{
    public override void Fly()
    {
        throw new NotImplementedException("I can't fly.");
    }
}

這種設計思路雖然可以解決問題,但不夠優美。因為除了鴕鳥之外,不會飛的鳥還有很多,比如企鵝。對於這些不會飛的鳥來說,我們都需要重寫 Fly() 方法,丟擲異常。

這違背了迪米特法則(也叫最少知識原則),暴露不該暴露的介面給外部,增加了類使用過程中被誤用的概率。

解決方案二

通過 AbstractBird 類派生出兩個更加細分的抽象類:會飛的鳥類 AbstractFlyableBird 和不會飛的鳥類 AbstractUnFlyableBird,讓麻雀、烏鴉這些會飛的鳥都繼承 AbstractFlyableBird,讓鴕鳥、企鵝這些不會飛的鳥,都繼承 AbstractUnFlyableBird 類。

此時,繼承關係變成了三層,還行得通。

如果要再新增一個游泳 Swim() 的方法,那情況就複雜了,要分為四中情況:

  • 會飛會游泳
  • 會飛不會游泳
  • 不會飛會游泳
  • 不會飛不會游泳

如果再有其他行為加入,抽象類的數量就會幾何級數增長。

我們要搞清楚某個類具有哪些方法、屬性,必須閱讀父類的程式碼、父類的父類的程式碼……一直追溯到最頂層父類的程式碼。

使用組合

針對“會飛”這樣一個行為特性,我們可以定義一個 Flyable 介面,只讓會飛的鳥去實現這個介面。針對會游泳,定義一個 Swimable 介面,會叫定義一個 Tweetable 介面。

public interface Flyable
{
    void Fly();
}

public interface Swimable
{
    void Swim();
}

public interface Tweetable
{
    void Tweet();
}

//麻雀
public class Sparrow : Flyable, Tweetable
{
    public void Fly() => Console.WriteLine("I am flying.");

    public void Tweet() => Console.WriteLine("!@#$%^&*……");
}

//企鵝
public class Penguin : Swimable, Tweetable
{
    public void Swim() => Console.WriteLine("I am swimming.");

    public void Tweet() => Console.WriteLine("!@#$%^&*……");
}

麻雀和企鵝都會叫,Tweet 實現了兩遍,這是壞味道。我們可以用組合來消除這個壞味道。

public interface Flyable
{
    void Fly();
}

public interface Swimable
{
    void Swim();
}

public interface Tweetable
{
    void Tweet();
}

public class FlyAbility : Flyable
{
    public void Fly() => Console.WriteLine("I am flying.");
}

public class SwimAbility : Swimable
{
    public void Swim() => Console.WriteLine("I am swimming.");
}

public class TweetAbility : Tweetable
{
    public void Tweet() => Console.WriteLine("!@#$%^&*……");
}

//麻雀
public class Sparrow : Flyable, Tweetable
{
    FlyAbility flyAbility = new FlyAbility();
    TweetAbility tweetAbility = new TweetAbility();

    public void Fly() => flyAbility.Fly();

    public void Tweet() => tweetAbility.Tweet();
}

//企鵝
public class Penguin : Swimable, Tweetable
{
    SwimAbility swimAbility = new SwimAbility();
    TweetAbility tweetAbility = new TweetAbility();

    public void Swim() => swimAbility.Swim();

    public void Tweet() => tweetAbility.Tweet();
}

雖然現在主流的思想都是多用組合少用繼承,但是從上面的例子可以看出,繼承改寫成組合意味著要做更細粒度的類的拆分,要定義更多的類和介面。類和介面的增多也就或多或少地增加程式碼的複雜程度和維護成本。所以,在實際的專案開發中,我們還是要根據具體的情況,來具體選擇該用繼承還是組合。


本文出自極客時間 王爭 老師的課程《設計模式之美》。原文示例為 java,因為我是做 C# 的,所以本文示例程式碼我改成了 C# 。

相關文章