超易懂!原來(lái) SOLID 原則要這么理解!
說(shuō)到 SOLID 原則,相信有過(guò)幾年工作經(jīng)驗(yàn)的朋友都有個(gè)大概印象,但就是不知道它具體是什么。甚至有些工作了十幾年的朋友,它們對(duì) SOLID 原則的理解也停留在表面。今天我們就來(lái)聊聊 SOLID 原則以及它們之間的關(guān)系。
01 什么是 SOLID 原則
SOLID 原則其實(shí)是用來(lái)指導(dǎo)軟件設(shè)計(jì)的,它一共分為五條設(shè)計(jì)原則,分別是:
- 單一職責(zé)原則(SRP)
- 開(kāi)閉原則(OCP)
- 里氏替換原則(LSP)
- 接口隔離原則(ISP)
- 依賴倒置原則(DIP)
單一職責(zé)原則(SRP)
單一職責(zé)原則(Single Responsibility Principle),它的定義是:應(yīng)該有且僅有一個(gè)原因引起類的變更。簡(jiǎn)單地說(shuō):接口職責(zé)應(yīng)該單一,不要承擔(dān)過(guò)多的職責(zé)。 用生活中肯德基的例子來(lái)舉例:負(fù)責(zé)前臺(tái)收銀的服務(wù)員,就不要去餐廳收盤(pán)子。負(fù)責(zé)餐廳收盤(pán)子的就不要去做漢堡。
單一職責(zé)適用于接口、類,同時(shí)也適用于方法。例如我們需要修改用戶密碼,有兩種方式可以實(shí)現(xiàn),一種是用「修改用戶信息接口」實(shí)現(xiàn)修改密碼,一種是新起一個(gè)接口來(lái)實(shí)現(xiàn)修改密碼功能。在單一職責(zé)原則的指導(dǎo)下,一個(gè)方法只承擔(dān)一個(gè)職能,所以我們應(yīng)該新起一個(gè)接口來(lái)實(shí)現(xiàn)修改密碼的功能。
單一職責(zé)原則的重點(diǎn)在于職責(zé)的劃分,很多時(shí)候并不是一成不變的,需要根據(jù)實(shí)際情況而定。單一職責(zé)能夠使得類復(fù)雜性降低、類之間職責(zé)清晰、代碼可讀性提高、更加容易維護(hù)。但它的缺點(diǎn)也很明顯,就是對(duì)技術(shù)人員要求高,有些時(shí)候職責(zé)難以區(qū)分。
我們?cè)谠O(shè)計(jì)一個(gè)類的時(shí)候,可以先從粗粒度的類開(kāi)始設(shè)計(jì),等到業(yè)務(wù)發(fā)展到一定規(guī)模,我們發(fā)現(xiàn)這個(gè)粗粒度的類方法和屬性太多,且經(jīng)常修改的時(shí)候,我們就可以對(duì)這個(gè)類進(jìn)行重構(gòu)了,將這個(gè)類拆分成粒度更細(xì)的類,這就是所謂的持續(xù)重構(gòu)。
開(kāi)閉原則(OCP)
開(kāi)閉原則(Open Closed Principle),它的定義是:一個(gè)軟件實(shí)體,如類、模塊和函數(shù)應(yīng)該對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉。簡(jiǎn)單地說(shuō):就是當(dāng)別人要修改軟件功能的時(shí)候,使得他不能修改我們?cè)写a,只能新增代碼實(shí)現(xiàn)軟件功能修改的目的。
這聽(tīng)著有點(diǎn)玄乎,我來(lái)舉個(gè)例子吧。
這段代碼模擬的是對(duì)于水果剝皮的處理程序。如果是蘋(píng)果,那么是一種撥皮方法;如果是香蕉,則是另一種剝皮方法。如果以后還需要處理其他水果,那么就會(huì)在后面加上很多 if else 語(yǔ)句,最終會(huì)讓整個(gè)方法變得又臭又長(zhǎng)。如果恰好這個(gè)水果中的不同品種有不同的剝皮方法,那么這里面又會(huì)有很多層嵌套。
- if(type == apple){
- //deal with apple
- } else if (type == banana){
- //deal with banana
- } else if (type == ......){
- //......
- }
可以看得出來(lái),上面這樣的代碼并沒(méi)有滿足「對(duì)拓展開(kāi)放,對(duì)修改封閉」的原則。每次需要新增一種水果,都可以直接在原來(lái)的代碼上進(jìn)行修改。久而久之,整個(gè)代碼塊就會(huì)變得又臭又長(zhǎng)。
如果我們對(duì)剝水果皮這件事情做一個(gè)抽象,剝蘋(píng)果皮是一個(gè)具體的實(shí)現(xiàn),剝香蕉皮是一個(gè)具體的實(shí)現(xiàn),那么寫(xiě)出的代碼會(huì)是這樣的:
- public interface PeelOff {
- void peelOff();
- }
- public class ApplePeelOff implement PeelOff{
- void peelOff(){
- //deal with apple
- }
- }
- public class BananaPeelOff implement PeelOff{
- void peelOff(){
- //deal with banan
- }
- }
- public class PeelOffFactory{
- private Map<String, PeelOff> map = new HashMap();
- private init(){
- //init all the Class that implements PeelOff interface
- }
- }
- .....
- public static void main(){
- String type = "apple";
- PeelOff peelOff = PeelOffFactory.getPeelOff(type); //get ApplePeelOff Class Instance.
- peelOff.pealOff();
- }
上面這種實(shí)現(xiàn)方式使得別人無(wú)法修改我們的代碼,為什么?
因?yàn)楫?dāng)需要對(duì)西瓜剝皮的時(shí)候,他會(huì)發(fā)現(xiàn)他只能新增一個(gè)類實(shí)現(xiàn) PeelOff 接口,而無(wú)法在原來(lái)的代碼上修改。這樣就實(shí)現(xiàn)了「對(duì)拓展開(kāi)放,對(duì)修改封閉」的原則。
里氏替換原則(LSP)
里氏替換原則(LSP)的定義是:所有引用基類的地方必須能透明地使用其子類的對(duì)象。簡(jiǎn)單地說(shuō):所有父類能出現(xiàn)的地方,子類就可以出現(xiàn),并且替換了也不會(huì)出現(xiàn)任何錯(cuò)誤。 例如下面 Parent 類出現(xiàn)的地方,可以替換成 Son 類,其中 Son 是 Parent 的子類。
- Parent obj = new Son();
- 等價(jià)于
- Son son = new Son();
這樣的例子在 Java 語(yǔ)言中是非常常見(jiàn)的,但其核心要點(diǎn)是:替換了也不會(huì)出現(xiàn)任何的錯(cuò)誤。這就要求子類的所有相同方法,都必須遵循父類的約定,否則當(dāng)父類替換為子類時(shí)就會(huì)出錯(cuò)。 這樣說(shuō)可能還是有點(diǎn)抽象,我舉個(gè)例子。
- public class Parent{
- // 定義只能扔出空指針異常
- public void hello throw NullPointerException(){
- }
- }
- public class Son extends Parent{
- public void hello throw NullPointerException(){
- // 子類實(shí)現(xiàn)時(shí)卻扔出所有異常
- throw Exception;
- }
- }
上面的代碼中,父類對(duì)于 hello 方法的定義是只能扔出空指針異常,但子類覆蓋父類的方法時(shí),卻扔出了其他異常,違背了父類的約定。那么當(dāng)父類出現(xiàn)的地方,換成了子類,那么必然會(huì)出錯(cuò)。
其實(shí)這個(gè)例子舉得不是很好,因?yàn)檫@個(gè)在編譯層面可能就有錯(cuò)誤。但表達(dá)的意思應(yīng)該是到位了。
而這里的父類的約定,不僅僅指的是語(yǔ)法層面上的約定,還包括實(shí)現(xiàn)上的約定。有時(shí)候父類會(huì)在類注釋、方法注釋里做了相關(guān)約定的說(shuō)明,當(dāng)你要覆寫(xiě)父類的方法時(shí),需要弄懂這些約定,否則可能會(huì)出現(xiàn)問(wèn)題。例如子類違背父類聲明要實(shí)現(xiàn)的功能。比如父類某個(gè)排序方法是從小到大來(lái)排序,你子類的方法竟然寫(xiě)成了從大到小來(lái)排序。
里氏替換原則 LSP 重點(diǎn)強(qiáng)調(diào):對(duì)使用者來(lái)說(shuō),能夠使用父類的地方,一定可以使用其子類,并且預(yù)期結(jié)果是一致的。
接口隔離原則(ISP)
接口隔離原則(Interface Segregation Principle)的定義是:類間的依賴關(guān)系應(yīng)該建立在最小的接口上。簡(jiǎn)單地說(shuō):接口的內(nèi)容一定要盡可能地小,能有多小就多小。
舉個(gè)例子來(lái)說(shuō),我們經(jīng)常會(huì)給別人提供服務(wù),而服務(wù)調(diào)用方可能有很多個(gè)。很多時(shí)候我們會(huì)提供一個(gè)統(tǒng)一的接口給不同的調(diào)用方,但有些時(shí)候調(diào)用方 A 只使用 1、2、3 這三個(gè)方法,其他方法根本不用。調(diào)用方 B 只使用 4、5 兩個(gè)方法,其他都不用。接口隔離原則的意思是,你應(yīng)該把 1、2、3 抽離出來(lái)作為一個(gè)接口,4、5 抽離出來(lái)作為一個(gè)接口,這樣接口之間就隔離開(kāi)來(lái)了。
那么為什么要這么做呢?我想這是為了隔離變化吧! 想想看,如果我們把 1、2、3、4、5 放在一起,那么當(dāng)我們修改了 A 調(diào)用方才用到 的 1 方法,此時(shí)雖然 B 調(diào)用方根本沒(méi)用到 1 方法,但是調(diào)用方 B 也會(huì)有發(fā)生問(wèn)題的風(fēng)險(xiǎn)。而如果我們把 1、2、3 和 4、5 隔離成兩個(gè)接口了,我修改 1 方法,絕對(duì)不會(huì)影響到 4、5 方法。
除了改動(dòng)導(dǎo)致的變化風(fēng)險(xiǎn)之外,其實(shí)還會(huì)有其他問(wèn)題,例如:調(diào)用方 A 抱怨,為什么我只用 1、2、3 方法,你還要寫(xiě)上 4、5 方法,增加我的理解成本。調(diào)用方 B 同樣會(huì)有這樣的困惑。
在軟件設(shè)計(jì)中,ISP 提倡不要將一個(gè)大而全的接口扔給使用者,而是將每個(gè)使用者關(guān)注的接口進(jìn)行隔離。
依賴倒置原則(DIP)
依賴倒置原則(Dependence Inversion Principle)的定義是:高層模塊不應(yīng)該依賴底層模塊,兩者都應(yīng)該依賴其抽象。抽象不應(yīng)該依賴細(xì)節(jié),即接口或抽象類不依賴于實(shí)現(xiàn)類。細(xì)節(jié)應(yīng)該依賴抽象,即實(shí)現(xiàn)類不應(yīng)該依賴于接口或抽象類。簡(jiǎn)單地說(shuō),就是說(shuō)我們應(yīng)該面向接口編程。通過(guò)抽象成接口,使各個(gè)類的實(shí)現(xiàn)彼此獨(dú)立,實(shí)現(xiàn)類之間的松耦合。
如果我們每個(gè)人都能通過(guò)接口編程,那么我們只需要約定好接口定義,我們就可以很好地合作了。軟件設(shè)計(jì)的 DIP 提倡使用者依賴一個(gè)抽象的服務(wù)接口,而不是去依賴一個(gè)具體的服務(wù)執(zhí)行者,從依賴具體實(shí)現(xiàn)轉(zhuǎn)向到依賴抽象接口,倒置過(guò)來(lái)。
02 SOLID 原則的本質(zhì)
我們總算把 SOLID 原則中的五個(gè)原則說(shuō)完了。但說(shuō)了這么一通,好像是懂了,但是好像什么都沒(méi)記住。 那么我們就來(lái)盤(pán)一盤(pán)他們之間的關(guān)系。ThoughtWorks 上有一篇文章說(shuō)得挺不錯(cuò),文中說(shuō):
- 單一職責(zé)是所有設(shè)計(jì)原則的基礎(chǔ),開(kāi)閉原則是設(shè)計(jì)的終極目標(biāo)。
- 里氏替換原則強(qiáng)調(diào)的是子類替換父類后程序運(yùn)行時(shí)的正確性,它用來(lái)幫助實(shí)現(xiàn)開(kāi)閉原則。
- 而接口隔離原則用來(lái)幫助實(shí)現(xiàn)里氏替換原則,同時(shí)它也體現(xiàn)了單一職責(zé)。
- 依賴倒置原則是過(guò)程式編程與面向?qū)ο缶幊痰姆炙畮X,同時(shí)它也被用來(lái)指導(dǎo)接口隔離原則。
圖片 l 來(lái)自 ThoughtWorks
簡(jiǎn)單地說(shuō):依賴倒置原則告訴我們要面向接口編程。當(dāng)我們面向接口編程之后,接口隔離原則和單一職責(zé)原則又告訴我們要注意職責(zé)的劃分,不要什么東西都塞在一起。
當(dāng)我們職責(zé)捋得差不多的時(shí)候,里氏替換原則告訴我們?cè)谑褂美^承的時(shí)候,要注意遵守父類的約定。而上面說(shuō)的這四個(gè)原則,它們的最終目標(biāo)都是為了實(shí)現(xiàn)開(kāi)閉原則。
參考資料
- 寫(xiě)了這么多年代碼,你真的了解 SOLID 嗎?- 知乎
- 如何理解 SOLID 原則?- ThoughtWorks 洞見(jiàn)
- 重構(gòu)的七宗罪 - ThoughtWorks 洞見(jiàn)
本文轉(zhuǎn)載自微信公眾號(hào)「陳樹(shù)義」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系陳樹(shù)義公眾號(hào)。