工作這么久,才明白的SOLID設(shè)計原則
前序
做C語言開發(fā)的應(yīng)該都知道,C是面向過程開發(fā)的,而c++是面向?qū)ο箝_發(fā)的。而封裝、繼承與多態(tài)是面向?qū)ο箝_發(fā)的三大特征。
但你可能不知道OOD(Object-Oriented Design)還有五大基本原則,被Bob大叔稱為SOLID原則,字母為每個原則的首字母,遵循這些原則能夠讓你的代碼在擴(kuò)展性、維護(hù)性以及重用性提高。而這些不正是我們所追求的嗎?
接下來我們就一塊學(xué)習(xí)學(xué)習(xí)這些原則,內(nèi)容較多,建議先收藏后反復(fù)觀看,文章末尾有很多參考鏈接。
五大基本原則-SOLID
1. SRP
SRP(The Single Responsibility Principle)單一職責(zé)原則。
SRP是SOLID五大設(shè)計原則中最容易被誤解的一個,SRP不就是每個模塊都應(yīng)該只做一件事嗎?非也非也,這只是在實(shí)現(xiàn)底層細(xì)節(jié)的實(shí)際原則,并非是SRP的全部。
SRP最初是這樣描述的:
任何一個模塊都應(yīng)該有且只有一個被修改的原因(There should never be more than one reason for a class to change)
SRP的定義幾經(jīng)迭代,最終被Robert C.Martin在《Clean Architecture》中定義為:
任何一個軟件模塊都應(yīng)該只對某一類行為者負(fù)責(zé)
那么上文中提到的軟件模塊究竟是指什么呢?大部分情況下,其最簡單的定義就是指一個源代碼文件。然而有些編程語言和編程環(huán)境并不是用源代碼文件來存儲程序的。在這些情況下,軟件模塊指的就是一組緊密相關(guān)的函數(shù)和數(shù)據(jù)結(jié)構(gòu)。
來看一個正面例子,C標(biāo)準(zhǔn)庫中的
再來看一個反面例子,還記得剛開始學(xué)習(xí)單片機(jī)編程的時候,項(xiàng)目工程中從始至終就一個main源文件,真的是連頭文件都不寫的,一個main里面包含了LED、按鍵等相關(guān)全部代碼,這明顯是不符合SRP這一原則的。就像下圖一樣。
2. OCP
OCP(The Open-Closed Principle)開放封閉原則。
OCP的定義如下:
軟件實(shí)體應(yīng)當(dāng)對擴(kuò)展開放,對修改關(guān)閉(Software entities should be open for extension,but closed for modification)
設(shè)計良好的計算機(jī)軟件應(yīng)該易于擴(kuò)展,同時抗拒修改。換句話說,一個良好的計算機(jī)系統(tǒng)應(yīng)該在不需要修改的前提下就可以輕易被擴(kuò)展。
OCP是系統(tǒng)框架設(shè)計的主導(dǎo)原則,其主要目的是讓系統(tǒng)易于擴(kuò)展,同時限制其每次被修改所影響的范圍。實(shí)現(xiàn)的方式是通過將系統(tǒng)劃分為一系列的組件,并且將這些組件的依賴關(guān)系按層次結(jié)構(gòu)進(jìn)行組織,使得高階的組件不會因低階組件被修改而受到影響。
我們來看一個網(wǎng)絡(luò)中協(xié)議的例子,如下圖:
圖中分為三個層級,最上方層級最高,每個層級有若干組件,高層級的組件依賴低層級的組件。3. LSP
LSP(LSP-Liskov Substitution Principle)里氏替換原則。
LSP定義:
使用基類對象指針或引用的函數(shù)必須能夠在不了解衍生類的條件下使用衍生類的對象(Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it)
里氏代換原則中說,任何基類可以出現(xiàn)的地方,子類一定可以出現(xiàn)。只有當(dāng)衍生類可以替換掉基類,軟件單位的功能不受到影響時,基類才能真正被復(fù)用,而衍生類也能夠在基類的基礎(chǔ)上增加新的行為。
我們來看一個經(jīng)典的反面案例,正方形(Square)與長方形(Rectangle)。
那這里提出疑問了,正方形和長方形之間的繼承關(guān)系是如何呢?
如果Ais-aB,則認(rèn)為B是基類,A為子類,A應(yīng)該繼承B。那這個簡單了,眾所周知正方形是長方形,立即推,長方形是基類,正方形是子類,正方形應(yīng)該繼承長方形。
基類長方形中有SetHeight()和SetWidth()方法,但是子類正方形有一個特點(diǎn),長和寬是相等的,所以在實(shí)現(xiàn)SetHeight()和SetWidth()都必須同時設(shè)置寬和高,我們來看代碼。
還是那句話任何基類可以出現(xiàn)的地方,子類一定可以出現(xiàn),所以在創(chuàng)建Rectangle對象指針的時候,這里其實(shí)給的是子類的對象,最終代碼運(yùn)行斷言報錯。
Assertion failed: rect->GetArea() == 10, file .\main.cpp, line 10
如果new出來的對象是Rectangle,則程序能夠正常執(zhí)行。如果new出來的是Square則會進(jìn)入斷言。從而Square不能代替Rectangle,所以不符合LSP原則,實(shí)際上Square并不是Rectangle的子類。
正方形是長方形,但是他們的行為并不一樣,所謂的行為,就是抽象出來的東西。正方形只要有一個設(shè)置邊長的方法就行了,而長方形需要設(shè)置寬和高兩種方法。
4. ISP
ISP(ISP-Interface Segregation Principle)接口隔離原則。
ISP定義:
不應(yīng)強(qiáng)制客戶端依賴于它們不使用的接口(Clients should not be forced to depend upon interfaces that they do not use.)
該原則還有另外一個定義:一個類對另一個類的依賴應(yīng)該建立在最小的接口上(The dependency of one class to another one should depend on the smallest possible interface)
假如現(xiàn)在有一個OPS類。用戶1只需要使用OPS類的op1方法,用戶2只需要OPS類的op2方法,但是呢,OPS類除了提供op1和op2方法還提供了若干方法。如下圖:
此時用戶2沒什么意見,心想著反正能實(shí)現(xiàn)我要的功能就可以了。
但是呢,用戶1不愿意,于是就去找開發(fā)人員理論,我就要實(shí)現(xiàn)一個op1功能,給我整這么多依賴干啥。除了這個功能,其他的全部都給我隱藏掉,下班之前我就要,說完頭一扭就走了。
開發(fā)人員心想,這么簡單的事情,讓我下班之前給你,這不是眼看人低嗎,說完就在用戶1和OPS之間又封裝一層IU1Ops接口,兩分鐘搞定。于是就去跟用戶1說,你用IU1Ops,里面有你要的接口,拿去用吧。說完頭一扭就走了。模型如下圖:
接口隔離原則和單一職責(zé)都是為了提高類的內(nèi)聚性、降低它們之間的耦合性,體現(xiàn)了封裝的思想,但兩者是不同的:
- 單一職責(zé)原則注重的是職責(zé),而接口隔離原則注重的是對接口依賴的隔離。
- 單一職責(zé)原則主要是約束類,它針對的是程序中的實(shí)現(xiàn)和細(xì)節(jié);接口隔離原則主要約束接口,主要針對抽象和程序整體框架的構(gòu)建。
5. DIP
DIP(DIP-Dependency Inversion Principle)依賴倒置原則。
DIP定義:
高層次的模塊不應(yīng)該依賴低層次的模塊,他們都應(yīng)該依賴于抽象(High level modules should not depend upon low level modules. Both should depend upon abstractions)
抽象不應(yīng)該依賴于具體實(shí)現(xiàn),具體實(shí)現(xiàn)應(yīng)該依賴于抽象(Abstractions should not depend upon details. Details should depend upon abstractions)
這個名字看著有點(diǎn)別扭,“依賴”還“倒置”,這到底是啥意思?
依賴指兩個相對獨(dú)立的對象,當(dāng)一個對象負(fù)責(zé)構(gòu)造另一個對象的實(shí)例,或者依賴另一個對象的服務(wù)時,這兩個對象之間主要體現(xiàn)為依賴關(guān)系。
老樣子,看示例,現(xiàn)在甲方需要能讓BMW車跑起來的功能,再看一下乙方的設(shè)計。
司機(jī)有駕駛BMW車輛的方法,BMW車輛有run的方法,所以是甲方滿足需求的。
后來甲方需求變了,甲方不僅要能開BMW車,也要能開Benz車,卻發(fā)現(xiàn)使用乙方原來的的設(shè)計Benz車卻開不起來,因?yàn)?driver()只接受BMW的車輛,不接受Benz的車輛,這明顯不合理,一個司機(jī)會開BMW卻不會開Benz,太反常了。于是去找乙方重新設(shè)計。
乙方也認(rèn)識到了不足,經(jīng)過多方討論終于有了如下設(shè)計。
對于每種車輛,都應(yīng)該有一個run的方法,所有的車輛都應(yīng)該繼承ICar實(shí)現(xiàn)。而司機(jī)呢,也不應(yīng)該依賴具體的車輛,而應(yīng)該依賴所有車輛的抽象方法。原本直接指向BMW的依賴箭頭被反置了?,F(xiàn)在即使甲方在添加別的車輛,司機(jī)仍然能開。不會出現(xiàn)只會開BMW而不會開其他車輛的情況了。
簡單來說,依賴倒轉(zhuǎn)原則就是指:代碼要依賴于抽象的類,而不要依賴于具體的類;要針對接口或抽象類編程,而不是針對具體類編程。根據(jù)上面的例子可以加深理解。
依賴倒置原則(Dependency Inversion Principle)是很多面向?qū)ο蠹夹g(shù)的根基。它特別適合應(yīng)用于構(gòu)建可復(fù)用的軟件框架,并且,因?yàn)槌橄蠛图?xì)節(jié)已經(jīng)彼此隔離,代碼也變得更易維護(hù)。
采用依賴倒置原則可以減少類間的耦合性,提高系統(tǒng)的穩(wěn)定性,減少并行開發(fā)引起的風(fēng)險,提高代碼的可讀性和可維護(hù)性。