SOLID 原則的價值
原則 | 核心價值 | 解決的問題 |
---|---|---|
SRP | 職責分離,提高內聚性 | 代碼臃腫、牽一發而動全身 |
OCP | 通過擴展而非修改實現變化 | 頻繁修改現有代碼導致的風險 |
LSP | 確保子類行為的一致性 | 繼承濫用導致的系統不穩定 |
ISP | 定制化接口,避免依賴冗余 | 接口過大導致的實現負擔 |
DIP | 解耦高層與低層模塊 | 模塊間強依賴導致的可維護性差 |
SOLID原則是一組面向對象編程和設計的五個基本原則,由羅伯特·C·馬丁在21世紀早期引入。這五個原則的首字母縮寫為SOLID,它們分別是:單一職責原則(Single Responsibility Principle)、開閉原則(Open-Closed Principle)、里氏替換原則(Liskov Substitution Principle)、接口隔離原則(Interface Segregation Principle)和依賴反轉原則(Dependency Inversion Principle)。
單一職責原則 (SRP)
單一職責原則 (SRP) 英文全稱為 Single Responsibility Principle,是最簡單,但也是最難用好的原則之一。它的定義也很簡單:對于一個類而言,應該僅有一個引起它變化的原因。其中變化的原因就表示了這個類的職責,它可能是某個特定領域的功能,可能是某個需求的解決方案。
單一職責原則用于控制類的粒度大小,減少類中不相關功能的代碼耦合,使得類更加的健壯;另外,單一職責原則也適用于模塊之間解耦,對于模塊的功能劃分有很大的指導意義。
這個原則表達的是不要讓一個類承擔過多的責任,一旦有了多個職責,那么它就越容易因為某個職責而被更改,這樣的狀態是不穩定的,不經意的修改很有可能影響到這個類的其他功能。因此,我們需要將不同的職責封裝在不同的類中,即將不同的變化原因封裝在不同的類中,不同類之間的變化互不影響。
面對違背單一職責原則的程序代碼,我們可以利用外觀模式,代理模式,橋接模式,適配器模式,命令模式 等對已有設計進行重構,實現多職責的分離。
我們在設計一個類的時候,可以先從粗粒度的類開始設計,等到業務發展到一定規模,我們發現這個粗粒度的類方法和屬性太多,且經常修改的時候,我們就可以對這個類進行重構了,將這個類拆分成粒度更細的類,這就是所謂的持續重構
開閉原則(OCP)
開閉原則 (OCP) 英文全稱為 Open-Closed Principle,基本定義是軟件中的對象(類,模塊,函數等)應該對于擴展是開放的,但是對于修改是封閉的。這里的對擴展開放表示這添加新的代碼,就可以讓程序行為擴展來滿足需求的變化;對修改封閉表示在擴展程序行為時不要修改已有的代碼,進而避免影響原有的功能。
要實現不改代碼的情況下,仍要去改變系統行為的關鍵就是抽象和多態,通過接口或者抽象類定義系統的抽象層,再通過具體類來進行擴展。這樣一來,無須對抽象層進行任何改動,只需要增加新的具體類來實現新的業務功能即可,達到開閉原則的要求
同樣,舉個例子來更深刻地理解開閉原則:有一個用于圖表顯示的 Display 類,它能繪制各種類型的圖表,比如餅狀圖,柱狀圖等;而需要繪制特定圖表時,都強依賴了對應類型的圖表,Display 類的內部實現如下:
public void display(String type) {if (type.equals("pie")) { PieChart chart = new PieChart(); chart.display(); } else if (type.equals("bar")) { BarChart chart = new BarChart(); chart.display(); }
}
基于上述的代碼,如果需要新增一個圖表,比如折線圖 LineChart ,就要修改 Display 類的 display() 方法,增加新增的判斷邏輯,很顯然這樣的做法違反開閉原則。而讓類的實現符合開閉原則的方式就是引入抽象圖表類 AbstractChart,作為其他圖表的基類,讓 Display 依賴這個抽象圖表類 AbstractChart,然后通過 Display 決定使用哪種具體的圖表類,實現代碼變成了這樣:
private Abstractchart chart;public void display() {chart.display();
}
現在我們新增一個 LineChart 對象只需要繼承Abstractchart 實現其方法,無須修改之前的 BarChart 的代碼。
這個重構后的設計符合開閉原則,因為我們通過擴展子類來實現新的功能,而不需要修改父類的代碼和其他子類的代碼。這樣做的好處是,已有的代碼保持不變,不會引入新的錯誤,同時也增加了系統的可擴展性和可維護性,但會增加一定的復雜性。
總結起來,開閉原則鼓勵我們在設計軟件時,采用抽象、封裝和多態等方式,使得系統能夠以最小的修改來適應變化。這種設計思想能夠提高代碼的可復用性、可擴展性和可維護性,是良好的軟件設計實踐之一。
里式替換原則 (LSP)
里式替換原則 (LSP) 英文全稱為 Liskov Substitution Principle,基本定義為:在不影響程序正確性的基礎上,所有使用基類的地方都能使用其子類的對象來替換。這里提到的基類和子類說的就是具有繼承關系的兩類對象,當我們傳遞一個子類型對象時,需要保證程序不會改變任何原基類的行為和狀態,程序能正常運作。 一句話概括為: 能夠使用父類的地方,一定可以使用其子類,并且預期結果是一致的。
讓我們舉一個簡單的例子比方說,我們有一個動物超類,它有一個叫做 "咆哮 "的方法。現在我們有了我們的子類,例如狗和熊擴展了超類Animal。現在我們知道狗不會吼叫,但熊會,所以我們必須拋出一個狗不能吼叫的異常。所以子類(狗)實際上不能使用超類的這個咆哮方法,那么我們就違反了這個原則。
所以,我們需要確保我們的子類能夠實現超類所有暴露的方法并且預期結果是一致的,這樣才不會違反原則。
另一方面,里式替換原則也是對開閉原則的補充,不僅適用于繼承關系,還適用于實現關系的設計,常提到的 IS-A (父子繼承關系) 是針對行為方式來說的,如果兩個類的行為方式是不相容,那么就不應該使用繼承,更好的方式是提取公共部分的方法來代替繼承
里氏替換主要解決的問題:
- 里氏替換解決了繼承中重寫父類造成的可復用性變差的問題
- 是動作正確性的保證,即類的擴展不會給已有系統引入新的錯誤,降低了代碼出錯的可能性
- 加強程序的健壯性,同時變更時可以做到非常好的兼容性,提高程序的維護性、可擴展性,降低需求變更時引入的風險。
接口隔離原則 (ISP)
接接口隔離原則(Interface Segregation Principle)的定義是:類間的依賴關系應該建立在最小的接口上。簡單地說:接口的內容一定要盡可能地小,能有多小就多小。
舉個例子來說,我們經常會給別人提供服務,而服務調用方可能有很多個。很多時候我們會提供一個統一的接口給不同的調用方,但有些時候調用方 A 只使用 1、2、3 這三個方法,其他方法根本不用。調用方 B 只使用 4、5 兩個方法,其他都不用。接口隔離原則的意思是,你應該把 1、2、3 抽離出來作為一個接口,4、5 抽離出來作為一個接口,這樣接口之間就隔離開來了。
那么為什么要這么做呢?我想這是為了隔離變化吧! 想想看,如果我們把 1、2、3、4、5 放在一起,那么當我們修改了 A 調用方才用到 的 1 方法,此時雖然 B 調用方根本沒用到 1 方法,但是調用方 B 也會有發生問題的風險。而如果我們把 1、2、3 和 4、5 隔離成兩個接口了,我修改 1 方法,絕對不會影響到 4、5 方法。
除了改動導致的變化風險之外,其實還會有其他問題,例如:調用方 A 抱怨,為什么我只用 1、2、3 方法,你還要寫上 4、5 方法,增加我的理解成本。調用方 B 同樣會有這樣的困惑。
依賴倒置原則(DIP)
依賴倒置原則 (DIP) 英文全稱 Dependency Inversion Principle, DIP),基本定義是:
- 高層模塊不應該依賴低層模塊,應該共同依賴抽象;
- 抽象不應該依賴細節,細節應該依賴抽象。
這里的抽象就是接口和抽象類,而細節就是實現接口或繼承抽象類而產生的類。
如何理解“高層模塊不應該依賴低層模塊,應該共同依賴抽象”呢?如果高層模塊依賴于低層模塊,那么低層模塊的改動很有可能影響到高層模塊,從而導致高層模塊被迫改動,這樣一來讓高層模塊的重用變得非常困難。
而最佳的做法就如上圖一樣,在高層模塊構建一個穩定的抽象層,并且只依賴這個抽象層;而由底層模塊完成抽象層的實現細節。這樣一來,高層類都通過該抽象接口使用下一層,移除了高層對底層實現細節的依賴。
依賴倒置原則可以減少類間的耦合性,提高系統的穩定性,降低并行開發引起的風險,提高代碼的可讀性和可維護性。同時依賴倒置原則也是框架設計的核心原則,善于創建可重用的框架和富有擴展性的代碼。
我們來看看一個非常簡單的寄信
案例幫你理解依賴倒置,寄信這個業務,主要存在兩種角色:寄信人和郵遞員。 最開始寄信人強依賴于郵遞員,寄信需要送到郵遞員家。這種模式缺點比較明顯,郵遞員換了很麻煩。直到后面增加了郵筒,寄信人不再直接依賴郵遞員,而是依賴一個站著不會動的郵筒。
上面的郵筒,可以讓郵遞員再怎么變化,都不會影響到寄信人。這種將寄信人直接依賴郵遞員,改為寄信人和郵遞員互不依賴,兩者都依賴于郵筒的過程,正是“依賴倒置”
下面來看看系統中模塊的設計使用依賴倒置的場景:
比如 Tomcat 容器的 Servlet 規范實現,Spring Ioc 容器實現就是第一層境界的產品, 我們熟悉的 RPC 模式就是,第二層境界的產品 。 那么我們是不是可以總結為第一層境界是在系統內部模塊和模塊的協作,第二層境界是跨系統協作。