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

来源:https://www.cnblogs.com/gl1573/archive/2020/06/15/13129284.html
-Advertisement-
Play Games

如果問面向對象的三大特性是什麼,多數人都能回答出來:封裝、繼承、多態。 繼承 作為三大特性之一,近來卻越來越不推薦使用,更有極端的語言,直接語法中就不支持繼承,例如 Go。這又是為什麼呢? 為什麼不推薦使用繼承? 假設我們要設計一個關於鳥的類。 我們將“鳥類”定義為一個抽象類 AbstractBir ...


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

繼承 作為三大特性之一,近來卻越來越不推薦使用,更有極端的語言,直接語法中就不支持繼承,例如 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# 。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 許多小伙伴關於WEB前端工程師是很有些想法的,畢竟是高薪又面子,誰都想瞭解一下,一探究竟。又不少人都個筆者留言,問WEB前端工程師要做什麼作業,有提升空間嗎,本文就是為了回答這個問題而誕生的,現在我們就一起來討論一下,WEB前端工程師要做什麼,有提升空間嗎,這個話題。 1:在知道WEB前端工程師要做 ...
  • 在我們做應用系統的時候,往往都會涉及圖表的展示,綜合的圖表展示能夠給客戶帶來視覺的享受和數據直觀體驗,同時也是增強客戶認同感的舉措之一。基於圖表的處理,我們一般往往都是利用對應第三方的圖表組件,然後在這個基礎上為它的數據模型提供符合要求的圖表數據即可,VUE+Element 前端應用也不例外,我們這... ...
  • 為元素添加on方法 Element.prototype.on = Element.prototype.addEventListener; NodeList.prototype.on = function (event, fn) {、 []['forEach'].call(this, function ...
  • config下麵的host不要動,只改動package.jsong下麵的host不要動,只改動package.json下dev即可 "scripts": { "dev": "webpack-dev-server --inline --progress --config build/webpack.d ...
  • <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Title</title> <link href="https://cdn.bootcss.com/openlayers/4.6.5/ol.css" rel="st ...
  • 今天給大家介紹一下全球最大的男性交友網站:Github。 度娘是這麼介紹的: 拿人話說,就是程式員們可以把自己原創的代碼發上去,供別人免費使用。 放到我們小編屆,就是唐唐寫一篇文章,免費放到網上讓別人轉載,只要標個出處,誰複製都行。 作為回報……emm代碼的上傳者並沒有任何回報。 Github社區建 ...
  • 現象:el-input或者input標簽中,輸入空格無效,並不能加入到字元串中。 原因:眾多原因之一: 1. 首先,這是在vue項目中,那麼檢查el-input或者input外層包的dom元素。 檢查是否有el-menu這個元素包在了el-input外層或者input外層,如果有,那麼el-menu ...
  • 一、UML解析器設計 ​ 先看下題目:第四單元實現一個基於JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的對象(元素)可能與同級元素連接,也可與低級元素相連形成 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...