深入理解設計模式六大原則

来源:https://www.cnblogs.com/mcbye/archive/2020/01/31/Introduction-to-designPattern-rules.html
-Advertisement-
Play Games

萬變不離其宗,不管是Java還是C++,凡是面向對象的編程語言,在設計上,儘管表現形式可能有所不同,但是其實質和所需遵守的原則都是一致的。本文便是帶領讀者去深入理解設計模式中的六大原則,以期幫助讀者做出更好的設計。 ...


深入理解設計模式六大原則

萬變不離其宗,不管是Java還是C++,凡是面向對象的編程語言,在設計上,儘管表現形式可能有所不同,但是其實質和所需遵守的原則都是一致的。本文便是帶領讀者去深入理解設計模式中的六大原則,以期幫助讀者做出更好的設計。

單一職責原則

單一職責原則:Single Responsibility Principle,簡稱SRP

定義

應該有且僅有一個原因引起類的變更。

問題場景

類C負責兩個不同的職責:職責D1,職責D2。當由於職責D1需求發生改變而需要修改類C時,有可能會導致原本運行正常的職責D2功能發生故障。

單一職責最難劃分的就是職責,一個職責一個介面,但問題是”職責“沒有一個量化的標準,一個類到底要負責哪些職責?這些職責怎麼細化?細化後是否都要有一個介面或類?這些都需要從實際的項目去考慮。

解決方案

遵循單一職責原則。分別建立兩個類C1、C2,使C1完成職責D1功能,C2完成職責D2功能。這樣,當修改類C1時,不會使職責D2發生故障風險;同理,當修改C2時,也不會使職責D1發生故障風險。

比如說一個用戶類,應該把用戶的信息抽取成一個BO(Business Object,業務對象),把行為抽取成一個Biz(Business Logic,業務邏輯)。這樣前者的職責是收集和反饋用戶的屬性信息;後者的職責是完成用戶信息的維護和變更。分成這樣的兩個介面來設計之後,這兩個職責的變化就不會互相影響。

單一職責的好處

  • 類的複雜性降低,實現什麼職責都有清晰明確的定義;
  • 可讀性提高;
  • 可維護性提高;
  • 變更引起的風險降低。

變更是必不可少的,如果介面的的單一職責做得好,一個介面修改只對相應的實現類有影響,對其他的介面無影響,這對系統的擴展性、維護性都有非常大的幫助。

里氏替換原則

里氏替換原則:Liskov Substitution Principle,簡稱LSP。這一原則最早在1988年,由麻省理工學院的一位叫做Barbara Liskov提出來的,所以將其命名為里氏替換原則。

定義一(標准定義)

如果對每一個類型為S的對象o1,都有類型為T的對象o2,使得以T定義的所有程式P在所有的對象o1都代換成o2時,程式P的行為沒有發生變化,那麼類型S是類型T的子類型。

定義二(通俗定義)

所有引用基類的地方必須能透明地使用其子類的對象。

從定義二中可以理解到,只要父類能出現的地方子類就可以出現,而且替換為子類也不會產生任何錯誤或異常,使用者可能根本不需要知道是父類還是子類。但反之就不行了。

里氏替換原則的規範

  1. 子類必須完全實現父類的方法

    • 在類中調用其他類時務必要使用父類或介面,如果不能使用父類或介面,則說明類的設計已經違背了LSP原則。
    • 如果子類不能完整地實現父類的方法,或者父類的某些方法在子類中已經發生”畸變“,則建議斷開父子繼承關係,採用依賴、聚集、組合等關係代替繼承。
    • 子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
  2. 子類可以有自己的個性

    子類中可以增加自己特有的方法。因為子類可能有比父類多的屬性和行為,所以向下轉型是不安全的,從LSP來看,就是有子類出現的地方父類未必就可以出現。

  3. 覆蓋或實現父類的方法時參數可以被放大

    LSP要求制定一個契約,就是父類或介面,這種設計方法也叫做Design by Contract。契約制定了,也就同時制定了前置條件(即方法的形參)和後置條件(即方法的返回值)。

    在實際應用中父類一般都是抽象類,子類是實現類,子類中方法的前置條件必須與超類中被覆寫的方法的前置條件相同或者更寬鬆。

  4. 覆寫或實現父類的方法時輸出結構可以被縮小

    父類的一個方法的返回值是一個類型T,子類的相同方法(重載或覆寫)的返回值為S,那麼LSP就要求S必須小於或等於T。

依賴倒置原則

定義

高層模塊不應該依賴低層模塊,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。

問題場景

類A直接依賴於類B,假如要將類A改為依賴於類C,則必須通過修改類A的代碼來達成。這種場景下,類A一般是高層模塊,類B和類C是低層模塊,假如修改了類A,可能會給程式帶來不必要的風險。

解決方案

將類A修改為依賴介面I,類B和類C各自實現介面I,類A通過介面I間接與類或者類C發生聯繫,則會大大降低修改類A的幾率。

依賴倒置原則的核心思想是面向介面編程。

依賴倒置原則基於這樣一個事實:相對於細節的多變性,抽象的東西要穩定的多。以抽象為基礎搭建起來的架構比以細節為基礎搭建起來的架構要穩定的多。在java中,抽象指的是介面或者抽象類,細節就是具體的實現類,使用介面或者抽象類的目的是制定好規範和契約,而不去涉及任何具體的操作,把展現細節的任務交給他們的實現類去完成。

依賴的三種寫法

依賴是可以傳遞的,對象的依賴關係有三種方式來傳遞:

  1. 構造函數傳遞依賴對象。在類中通過構造函數聲明依賴對象,按照依賴註入的說法,這種方式叫作構造函數註入;
  2. Setter方法傳遞依賴對象。在抽象類中設置Setter方法聲明依賴關係,依照依賴註入的說法,這是Setter依賴註入;
  3. 介面聲明依賴對象。在介面的方法中聲明依賴對象,這種方法也叫做介面註入。

最佳實踐

依賴倒置原則的本質就是通過抽象(介面或抽象類)使各個類或模塊的實現彼此獨立,不互相影響,實現模塊間的松耦合。在實際項目中,如何應用這個規則呢,只要遵循以下幾個規則就可以:

  • 每個類儘量都有介面或抽象類,或者抽象類和介面兩者都具備,這是依賴倒置的基本要求,有了抽象才可能依賴倒置;
  • 變數的錶面類型儘量是介面或者是抽象類;
  • 任何類都不應該從具體類派生;
  • 儘量不要覆寫基類的方法;
  • 結合里氏替換原則使用:介面負責定義public屬性和方法,並且聲明與其他對象的依賴關係,抽象類負責公共構造部分的實現,實現類準確的實現業務邏輯,同時在適當的時候對父類進行細化。

介面隔離原則

定義

客戶端不應該依賴它不需要的介面;一個類對另一個類的依賴應該建立在最小的介面上。

介面分為兩種:

  • 實例介面:在Java中聲明一個類,然後用new關鍵字產生一個實例,它是對一個類型的事物的描述,這是一種介面,從這個角度來看,Java中的類也是一種介面;
  • 類介面:Java中經常使用的interface關鍵字定義的介面。

什麼是隔離呢?它有兩種定義:

  • 客戶端不應該依賴它不需要的介面;
  • 類間的依賴關係應該建立在最小的介面上。

這兩句話可以概括為一句話:建立單一介面,不要建立臃腫龐大的介面。更通俗的講:介面儘量細化,同時介面中的方法儘量少。

問題由來

類A通過介面I依賴類B,類C通過介面I依賴類D,如果介面I對於類A和類B來說不是最小介面,則類B和類D必須去實現他們不需要的方法。

解決方案

將臃腫的介面I拆分為獨立的幾個介面,類A和類C分別與他們需要的介面建立依賴關係。也就是採用介面隔離原則。

介面隔離原則 vs. 單一職責原則:

二者的審視角度不同,單一職責要求的是類和介面職責單一,註重的是職責,這是業務邏輯上的劃分,而介面隔離原則要求介面的方法儘量少,它要求”儘量使用多個專門的介面“。

最佳實踐

  • 介面要儘量小,一個介面只服務於一個子模塊或業務邏輯,根據介面隔離原則拆分介面時,首先必須滿足單一職責原則;
  • 介面要高內聚,具體來講就是在介面中儘量少公佈public方法,介面是對外的承諾,承諾越少對系統的開發越有利,變更的風險也就越少,同時也有利於降低成本;
  • 已經被污染了的介面,儘量去修改,若變更的風險較大,則採用適配器模式進行轉化處理;
  • 定製服務:定製服務是單獨為一個個體提供優良的服務,要求是只提供訪問者需要的方法;
  • 介面設計是有限度的:介面設計的粒度需要根據經驗和常識進行合理的判斷。

迪米特法則

定義

Law of Demeter,簡稱LoD,也稱為最少知識原則,Least Knowledge Principle,簡稱LKP,兩個名字含義相同:一個對象應該對其他對象有最少的瞭解,即一個類應該對自己需要耦合或調用的類知道得最少,只關註自己調用的public方法,其他的一概不關心。

問題由來

類與類之間的關係越密切,耦合度越大,當一個類發生改變時,對另一個類的影響也越大。

最佳實踐

迪米特法則的核心思想就是類間解耦,弱耦合,只有弱耦合了以後,類的復用率才可以提高。其要求的結果就是產生了大量的中轉或跳轉類,導致系統的複雜性提高,同時也為維護帶來了的難度。因此在採用迪米特法則時,既要做到讓結構清晰,又做到高內聚低耦合。

開閉原則

定義

一個軟體實體類,如類、模塊和函數應該對擴展開放,對修改關閉。

問題由來

在軟體的生命周期內,因為變化、升級和維護等原因需要對軟體原有代碼進行修改時,可能會給舊代碼中引入錯誤,也可能會使我們不得不對整個功能進行重構,並且需要原有代碼經過重新測試。

最佳實踐

開閉原則是一個非常虛的原則,前面5個原則是對開閉原則的具體解釋,但是開閉原則並不局限於這麼多。在實際工作中需要註意以下幾點:

  • 抽象約束:抽象是對一組事物的通用描述,沒有具體的實現,也就表示它可以有非常多的可能性,可以跟隨需求的變化而變化。因此介面或抽象類可以約束一組可能變化的行為,並且能夠實現對擴展開放。
  • 元數據控制模塊行為:元數據是用來描述環境和數據的數據,通俗地講就是配置參數,參數可以從文件中獲得,也可以從資料庫中獲得,使用此方法的極致就是控制反轉,使用最多的就是Spring容器。
  • 制定項目章程:對項目來說,約定優於配置。章程中指定了所有人員都必須遵守的約定。
  • 封裝變化:將相同的變化封裝到一個介面或抽象類中;將不同的變化封裝到不同的介面或抽象類中,不應該有兩個不同的變化出現在同一個介面或抽象類中。封裝變化,也就是受保護的變化,找出預計有變化或不穩定的點,為這些變化點創建穩定的介面。

六大設計原則應用

理解

從整體上來理解六大設計原則,可以簡要的概括為一句話,用抽象構建框架,用實現擴展細節,具體到每一條設計原則,則對應一條註意事項:

  • 單一職責原則告訴我們實現類要職責單一;
  • 里氏替換原則告訴我們不要破壞繼承體系;
  • 依賴倒置原則告訴我們要面向介面編程;
  • 介面隔離原則告訴我們在設計介面的時候要精簡單一;
  • 迪米特法則告訴我們要降低耦合;
  • 開閉原則是總綱,告訴我們要對擴展開放,對修改關閉。

遵守

理解了這六大設計原則之後,如何來遵守呢?制定這六條原則的目的並不是要我們刻板的遵守,而是根據實際需要靈活運用。只要對它們的遵守程度在一個合理的範圍內,就算是良好的設計,用一幅圖來說明一下:

設計模式六邊形

圖中的每一條維度各代表一項原則,我們依據對這項原則的遵守程度在維度上畫一個點,則如果對這項原則遵守的合理的話,這個點應該落在紅色的同心圓內部;如果遵守的差,點將會在小圓內部;如果過度遵守,點將會落在大圓外部。一個良好的設計體現在圖中,應該是六個頂點都在同心圓中的六邊形。

六種設計模式圖形樣例

在上圖中,設計1、設計2屬於良好的設計,他們對六項原則的遵守程度都在合理的範圍內;設計3、設計4設計雖然有些不足,但也基本可以接受;設計5則嚴重不足,對各項原則都沒有很好的遵守;而設計6則遵守過渡了,設計5和設計6都是迫切需要重構的設計。

關註我的公眾號,獲取更多關於面試、技術的文章及福利資源。

Dali王的技術博客公眾號

【參考資料】

《設計模式之禪》

《大話設計模式》


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

-Advertisement-
Play Games
更多相關文章
  • 小火箭返回頂部案例 1. 滾動頁面,當頁面距離頂部超出1000px,顯示小火箭。 封裝在scroll函數里,當前頁面距離頂部為$(window).scrollTop >=1000 小火箭顯示和隱藏用fadeIn和fadeOut //當頁面超出1000px的時候,讓小火箭顯示,如果小於1000px,則 ...
  • 背景樣式 • background-color 設置元素的背景顏色。• background-image 把圖像設置為背景。• background-position 設置背景圖像的起始位置。• background-attachment 背景圖像是否固定或者隨著頁面的其餘部分滾動。• backgr ...
  • width和height是指content內容的寬高 當width的值小於min-width時,則寬度為min-width 當width的值大於max-width時,則寬度為max-width min-width和max-width存在相容性問題,在IE6以下不支持 哪些元素可以設置寬高屬性 塊級元 ...
  • 1.簡單使用: 當input裡面的值發生變化的時候,就會自動把變化後的值,綁定到Vue對象上去了 <body> <div id="app"> <input v-model="name" />{{name}}<br /> <textarea v-model="t"></textarea>{{t}}<b ...
  • font-family屬性值:具體字體名或者字體集 如果是中文或者有單詞之間有空格,需要加雙引號 字體集: Serif (有裝飾線) Sans-serif (無裝飾線) Monospace Cursive Fantasy <!DOCTYPE html> <html lang="en"> <head> ...
  • 選擇器權值: 標簽選擇器:1 類選擇器和偽類選擇器:10 ID選擇器:100 通配符選擇器:0 行內樣式:1000 !important 在一定條件下,優先順序最高 常用的css樣式命名 頁面結構頁頭:header頁面主體:main頁尾:footer內容:content/container容器: co ...
  • function a(){} 和 var a = function(){}的區別: 學習做浮窗,看到別人的代碼里有: window.onresize = function(){ chroX = document.documentElement.clientWidth;//yemian整個的高寬 ch ...
  • 本章介紹UML建模元素 1:Stereotype-也被稱為類型、構造型 UML里的元素擴展,簡單來說其功能就是在已有的類型上添加一些標記,類似於打個戳,從而生成新的東西。 簡單的說加一句話來更加清楚準確描述這個類。 2:Actor(主角、參與者)-是在系統之外與系統交互的某人或某事物,在建模過程中處 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...