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

若是問面向對象的三大特性是什麼,多數人都能回答出來:封裝、繼承、多態。java

繼承 做爲三大特性之一,近來卻愈來愈不推薦使用,更有極端的語言,直接語法中就不支持繼承,例如 Go。這又是爲何呢?設計模式

爲何不推薦使用繼承?

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

咱們將「鳥類」定義爲一個抽象類 AbstractBird。全部更細分的鳥,好比麻雀、鴿子、烏鴉等,都繼承這個抽象類。ui

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

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

解決方案一對象

在鴕鳥這個子類中重寫 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# 。

相關文章
相關標籤/搜索