淺談 SOLID 原則的具體使用

来源:http://www.cnblogs.com/OceanEyes/archive/2016/06/01/overview-of-solid-principles.html
-Advertisement-
Play Games

SOLID 是面向對象設計5大重要原則的首字母縮寫,當我們設計類和模塊時,遵守 SOLID 原則可以讓軟體更加健壯和穩定。那麼,什麼是 SOLID 原則呢?本篇文章我將談談 SOLID 原則在軟體開發中的具體使用。 單一職責原則(SRP) 單一職責原則(SRP)表明一個類有且只有一個職責。一個類就像 ...


SOLID 是面向對象設計5大重要原則的首字母縮寫,當我們設計類和模塊時,遵守 SOLID 原則可以讓軟體更加健壯和穩定。那麼,什麼是 SOLID 原則呢?本篇文章我將談談 SOLID 原則在軟體開發中的具體使用。

單一職責原則(SRP)

單一職責原則(SRP)表明一個類有且只有一個職責。一個類就像容器一樣,它能添加任意數量的屬性、方法等。然而,如果你試圖讓一個類實現太多,很快這個類就會變得笨重。任意小的改變都將導致這個單一類的變化。當你改了這個類,你將需要重新測試一遍。如果你遵守 SRP,你的類將變得簡潔和靈活。每一個類將負責單一的問題、任務或者它關註的點,這種方式你只需要改變相應的類,只有這個類需要再次測試。SRP 核心是把整個問題分為小部分,並且每個小部分都將通過一個單獨的類負責。

假設你在構建一個應用程式,其中有個模塊是根據條件搜索顧客並以Excel形式導出。隨著業務的發展,搜索條件會不斷增加,導出數據的分類也會不斷增加。如果此時將搜索與數據導出功能放在同一個類中,勢必會變的笨重起來,即使是微小的改動,也可能影響其他功能。所以根據單一職責原則,一個類只有一個職責,故創建兩個單獨的類,分別處理搜索以及導出數據。

開放封閉原則(OCP)

開放封閉原則(OCP)指出,一個類應該對擴展開放,對修改關閉。這意味一旦你創建了一個類並且應用程式的其他部分開始使用它,你不應該修改它。為什麼呢?因為如果你改變它,很可能你的改變會引發系統的崩潰。如果你需要一些額外功能,你應該擴展這個類而不是修改它。使用這種方式,現有系統不會看到任何新變化的影響。同時,你只需要測試新創建的類。

假設你現在正在開發一個 Web 應用程式,包括一個線上納稅計算器。用戶可以訪問Web 頁面,指定他們的收入和費用的細節,並使用一些數學公式來計算應納稅額。考慮到這一點,你創建瞭如下類:

public class TaxCalculator
{
    public decimal Calculate(decimal income, decimal deduction, string country)
    {
        decimal taxAmount = 0;
        decimal taxableIncome = income - deduction;
        switch (country)
        {
            case "India":
                //Todo calculation
                break;
            case "USA":
                //Todo calculation 
                break;
            case "UK":
                //Todocalculation
                break;
        }
        return taxAmount;
    }
}

這個方法非常簡單,通過指定收入和支出,可以動態切換不同的國家計算不同的納稅額。但這裡隱含了一個問題,它只考慮了3個國家。當這個 Web 應用變得越來越流行時,越來越多的國家將被加進來,你不得不去修改 Calculate 方法。這違反了開放封閉原則,有可能你的修改會導致系統其他模塊的崩潰。

讓我們對這個功能進行重構,以符合對擴展是開放,對修改是封閉的。

根據類圖,可以看到通過繼承實現橫向的擴展,並且不會引發對其他不相關類的修改。這時 TaxCalculator 類中的 Calculate 方法會異常簡單:

public decimal Calculate(CountryTaxCalculator obj)
{
    decimal taxAmount = 0;
    taxAmount = obj.CalculateTaxAmount();
    return taxAmount;
}

里氏替換原則(LSP)

里氏替換原則指出,派生的子類應該是可替換基類的,也就是說任何基類可以出現的地方,子類一定可以出現。值得註意的是,當你通過繼承實現多態行為時,如果派生類沒有遵守LSP,可能會讓系統引發異常。所以請謹慎使用繼承,只有確定是“is-a”的關係時才使用繼承。

假設你在開發一個大的門戶網站,並提供很多定製的功能給終端用戶,根據用戶的級別,系統提供了不同級別的設定。考慮到這個需求,設計如下類圖:

可以看到,ISettings 介面有 GlobalSettings、SectionSettings 以及 UserSettings 三個不同的實現。GlobalSettings 設置會影響整個應用程式,例如標題、主題等。SectionSettings 適用於門戶的各個部分,如新聞、天氣、體育等設置。UserSettings 為特定登錄用戶設置,如電子郵件和通知偏好。

這樣的設計沒問題,但如果有另一個需求,系統需要支持游客訪問,唯一區別是游客不支持系統的設定,為了滿足這個需求,你可能會如下設計:

public class GuestSettings : ISettings
{
    public void GetSettings()
    {
        //get settings from database
        //include guest name、ip address...
    }

    public void SetSettings()
    {
        //guests are not allowed set settings
        throw new NotImplementedException();
    }
}

這樣沒問題嗎?準確來說,系統存在隱患。當單獨使用 GuestSettings 時,因為我們瞭解游客不能設置,所以我們潛意識並不會主動調用 SetSettings 方法。但是由於多態,ISettings 介面的實現可以被替換為 GuestSettings 對象,當調用SetSettings 方法時,可能會引發系統異常。

重構這個功能,拆分為兩個不同的介面:IReadableSettings 和 IWritableSettings。子類根據需求實現所需的介面。

介面隔離原則(ISP)

介面隔離原則(ISP)表明類不應該被迫依賴他們不使用的方法,也就是說一個介面應該擁有儘可能少的行為,它是精簡的,也是單一的。

假設你正在開發一個電子商務的網站,需要有一個購物車和關聯訂單處理機制。你設計一個介面 IOrderProcessor,它用包含一個驗證信用卡是否有效的方法(ValidateCardInfo)以及收件人地址是否有效的方法(ValidateShippingAddress)。與此同時,創建一個OnlineOrderProcessor 的類表示線上支付。

這非常好,你的網站也能正常工作。現在讓我們來考慮另一種情形,假設線上信用卡支付不再有效,公司決定接受貨到付款支付。
乍一看,這個解決方案聽起來很簡單,你可以創建一個CashOnDeliveryProcessor 並實現 IOrderProcessor 介面。貨到付款的購買方式不會涉及任何信貸卡驗證,所以,CashOnDeliveryOrderProcessor 類內部的 ValidateCardInfo 方法拋出 NotImplementedException。

這樣的設計在未來可能會出現的潛在問題。假設由於某種原因線上信用用卡付款需要額外的驗證步驟。自然,IOrderProcessor 將被修改,它將包括那些額外的方法,於此同時 OnlineOrderProcessor 將實現這些額外的方法。然而,CashOnDeliveryOrderProcessor 儘管不需要任何的附加功能,但你必須實現這些附加的功能。顯然,這違反了介面隔離原則。

你需要將這個功能重構:

新的設計分成兩個介面。IOrderProcessor 介面只包含兩個方法:ValidateShippingAddress 和 ProcessOrder,而 ValidateCardInfo 抽象到到一個單獨的介面:IOnlineOrderProcessor。現在,線上信用卡支付的任何改變只局限於IOnlineOrderProcessor 和它的子類實現,而 CashOnDeliveryOrderProcessor 是不會被影響。因此,新設計符合介面隔離原則。

依賴倒置原則(DIP)

依賴倒置原則(DIP)表明高層模塊不應該依賴低層模塊,相反,他們應該依賴抽象類或者介面。這意味著你不應該在高層模塊中使用具體的低層模塊。因為這樣的話,高層模塊變得緊耦合低層模塊。如果明天,你改變了低層模塊,那麼高層模塊也會被修改。根據DIP原則,高層模塊應該依賴抽象(以抽象類或者介面的形式),低層模塊也是如此。通過面向介面(抽象類)編程,緊耦合被移除。

那麼什麼是高層模塊,什麼是低層模塊呢?通常情況下,我們會在一個類(高層模塊)的內部實例化它依賴的對象(低層模塊),這樣勢必造成兩者的緊耦合,任何依賴對象的改變都將引起類的改變。

依賴倒置原則表明高層模塊、低層模塊都依賴於抽象,舉個例子,你現在正在開發一個通知系統,當用戶改變密碼時,郵件通知用戶。

public class UserManager
{

    public void ChangePassword(string username,string oldpwd,string newpwd)
    {
        EmailNotifier notifier = new EmailNotifier();

        //add some logic and change password 
        //Notify the user
        notifier.Notify("Password was changed on "+DateTime.Now);
    }
}

這樣的實現在功能上沒有問題,但試想一下,新的需求希望通過SNS形式通知用戶,那麼我們只能手動將EmaiNorifier 替換為 SNSNotifier。在這兒,UserManager 就是高層模塊,而EmailNotifier 就是低層模塊,他們彼此耦合。我們希望解耦,依賴於抽象 INotifier,也就是面向介面的編程。

小結

本篇博客為大家介紹了面向對象設計的 SOLID 原則,並以具體的案例輔助講解。你可以看到,繼承和多態在SOLID 原則中扮演了非常重要的角色。我們的應用程式不能過度設計,當然也不能隨意設計。瞭解基本的 SOLID 原則能讓你的應用程式變得健壯。你可以在Github 上查看具體的示例代碼:https://github.com/MEyes/SOLID.Principles


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

-Advertisement-
Play Games
更多相關文章
  • 編譯器到底做了什麼實現的虛函數的晚綁定呢?我們來探個究竟。 編譯器對每個包含虛函數的類創建一個表(稱為V TA B L E)。在V TA B L E中,編譯器放置特定類的虛函數地址。在每個帶有虛函數的類 中,編譯器秘密地置一指針,稱為v p o i n t e r(縮寫為V P T R),指向這個對 ...
  • 適用場合: 7.3 工廠模式的適用場合 創建新對象最簡單的辦法是使用new關鍵字和具體類。只有在某些場合下,創建和維護對象工廠所帶來的額外複雜性才是物有所值。本節概括了這些場合。 7.3.1 動態實現 如果需要像前面自行車的例子一樣,創建一些用不同方式實現同一介面的對象,那麼可以使用一個工廠方法或簡 ...
  • http://blog.csdn.net/hil2000/article/details/41261267/ 一.我為什麼要學習go語言 當今已經是移動和雲計算時代,Go出現在了工業向雲計算轉型的時刻,簡單、高效、內 置併發原語和現代的標準庫讓Go語言尤其適合雲端軟體開發(畢竟它就是為此而設計的)。 ...
  • 上一篇 從引用傳遞到設計模式 (上) 的文末,提到非虛擬介面 NVI 的實現,即將虛函數聲明為保護型或私有型,藉由模板函數模式來實現 。 園友 @KillU 看的很仔細,提出了一個問題:虛函數是 private 類型,繼承可以麽? 答案是:可以 5 實現權和調用權 <Effective C++> 中 ...
  • 需求描述: 解決過程: 百度一番無果,google一番有了答案。 解決方案: android6.0(api=23)以後直接打開文件,讓系統去判斷如何處理。詳細解決方案見如下地址: http://www.jianshu.com/p/d896a09b9aca 原因分析: http://stackover ...
  • Installation Requirements: Eclipse 4.5 (Mars) or later. Java VM version 8 or later. Gocode and Go oracle. Instructions: For an Eclipse package without ...
  • 這裡來講解一下Java8 新特性中的函數式介面, 以及和Lambda 表達式的關係。看到過很多不少介紹Java8特性的文章,都會介紹到函數式介面和lambda表達式,但是都是分別介紹,沒有將兩者的關係說明清楚,在這裡,把自己的理解整理如下: 一、函數式介面: 函數式介面其實本質上還是一個介面,但是它 ...
  • FastDFS是一個開源的輕量級分散式文件系統,由跟蹤伺服器(tracker server)、存儲伺服器(storage server)和客戶端(client)三個部分組成,主要解決了海量數據存儲問題,特別適合以中小文件(建議範圍:4KB < file_size <500MB)為載體的線上服務。 S ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...