S.O.L.I.D 是面向?qū)ο笤O(shè)計(jì)(OOD)和面向?qū)ο缶幊?OOP)中的幾個(gè)重要編碼原則(Programming Priciple)的首字母縮寫。
SRP | The Single Responsibility Principle | 單一職責(zé)原則 |
OCP | The Open Closed Principle | 開放封閉原則 |
LSP | The Liskov Substitution Principle | 里氏替換原則 |
ISP | The Interface Segregation Principle | 接口分離原則 |
DIP | The Dependency Inversion Principle | 依賴倒置原則 |
一、單一職責(zé)原則(SRP)
從面向?qū)ο蠼嵌冉忉屵@個(gè)原則為:”引起類變化的因素永遠(yuǎn)不要多于一個(gè)。” 或者說(shuō) “一個(gè)類有且僅有一個(gè)職責(zé)”。這似乎不太好理解,特別是”引起類變化的因素永遠(yuǎn)不要多于一個(gè)。”這句話更是有點(diǎn)虛,讓人有點(diǎn)摸不著頭腦。
我們通常都說(shuō)“低耦合,高內(nèi)聚”。在我看來(lái),這里的”單一職責(zé)”就是我們通常所說(shuō)的“高內(nèi)聚”,即一個(gè)類只完成它應(yīng)該完成的職責(zé),不能推諉責(zé)任,也不可越殂代皰,不能成為無(wú)所不能的上帝類。如果你的團(tuán)隊(duì)中實(shí)施寬松的“代碼集體所有權(quán)”,在編碼的過(guò)程中出現(xiàn)許多人同時(shí)修改(維護(hù))同一個(gè)類的現(xiàn)象,而且成員之間的溝通不夠及時(shí),主動(dòng)和暢通的話,那么時(shí)間一長(zhǎng),就很可能出現(xiàn)“承擔(dān)過(guò)多職責(zé)”的上帝類。這時(shí),提煉基類/接口和提煉類重構(gòu)將能幫助我們消除或減輕這種設(shè)計(jì)臭味。
看一個(gè)例子:
這是一個(gè)違反了“單一職責(zé)原則” 的類結(jié)構(gòu)圖。
這里,Rectangle類做了下面兩件事:
- 計(jì)算矩形面積;
- 在界面(繪制設(shè)備)上繪制矩形;
并且,有兩個(gè)應(yīng)用使用了Rectangle類:
- 計(jì)算幾何應(yīng)用程序(Computational Geometry Application)用這個(gè)類計(jì)算面積;
- 圖形程序(Graphical Application)用這個(gè)類在界面上繪制矩形;
這違反了SRP(單一職責(zé)原則)。因?yàn)镽ectangle類做了兩件事,在一個(gè)方法里它計(jì)算了面積,在另外一個(gè)方法了它返回一個(gè)表示矩形的GUI。這會(huì)帶來(lái)一些有趣的問(wèn)題:在計(jì)算幾何應(yīng)用程序中我們必須包含GUI。也就是在開發(fā)幾何應(yīng)用時(shí),我們必須引用GUI庫(kù);圖形應(yīng)用程序中Rectangle類的變化可能導(dǎo)致計(jì)算幾何應(yīng)用程序的變化,編譯和測(cè)試,反之亦然。那么,怎么修改才能讓其符合單一職責(zé)原則呢?
答案是:拆分!拆分職責(zé)到兩個(gè)不同的類中,如:
- Rectangle: 這個(gè)類應(yīng)該只定義Area()方法;
- RectangleUI: 這個(gè)類應(yīng)繼承Rectangle類,并定義Draw()方法。
二、開放封閉原則 (OCP)
從面向?qū)ο笤O(shè)計(jì)角度看,這個(gè)原則可以這么理解:”軟件實(shí)體(類,模塊,函數(shù)等等)應(yīng)當(dāng)對(duì)擴(kuò)展開放,對(duì)修改閉合。” 通俗來(lái)講,它意味著你(或者類的客戶)應(yīng)當(dāng)能在不修改一個(gè)類的前提下擴(kuò)展這個(gè)類的行為。在OOD里,對(duì)擴(kuò)展開放意味著類或模塊的行為能夠改變,在需求變化時(shí)我們能以新的,不同的方式讓模塊改變,或者在新的應(yīng)用中滿足需求。
也就是說(shuō),對(duì)擴(kuò)展是開放的,而對(duì)修改是封閉的。我們通常都說(shuō):向系統(tǒng)中增加功能時(shí)應(yīng)該只是添加新代碼,而應(yīng)該盡量少的修改原代碼。在我看來(lái),這就是遵循開放封閉原則所能帶來(lái)的效果。曾經(jīng)在網(wǎng)上看到過(guò)這樣一句話“哪里變化,封裝哪里”。這其實(shí)就是說(shuō),我們要將系統(tǒng)中可能變化的地方封裝起來(lái),即對(duì)修改封閉。同時(shí),為了應(yīng)對(duì)系統(tǒng)需求(功能)的擴(kuò)展,需要抽象!
這里抽象是關(guān)鍵。《設(shè)計(jì)模式》中的state模式和strategy模式是這個(gè)原則的最好體現(xiàn)。
舉一個(gè)例子:
違反了開放封閉原則的類結(jié)構(gòu)圖。
客戶端代碼直接面向服務(wù)器端的具體實(shí)現(xiàn)編程,缺乏靈活性。這樣如果服務(wù)器因?yàn)槟承┰虮黄渌?wù)器替換了,那么客戶端調(diào)用服務(wù)器的代碼也必須做相應(yīng)的修改或替換。這其實(shí)就是”面向?qū)崿F(xiàn)編程“的設(shè)計(jì)臭味!
那么,如何修改才能得到正確靈活的設(shè)計(jì)?
答案是:抽象!為服務(wù)器端的代碼(類型)抽象出一個(gè)抽象基類(定義一組完成服務(wù)職責(zé)的最小接口)。
下面是正確的設(shè)計(jì):
遵循開放封閉原則的類結(jié)構(gòu)圖。
基本上,你抽象的東西是你系統(tǒng)的核心內(nèi)容,如果你抽象得好,很可能增加一個(gè)新的服務(wù)器類型(擴(kuò)展)只需要添加新類型(繼承自AbstractServer即可)。因此代碼要盡可能以抽象(這里的AbstractServer)為依據(jù),這會(huì)允許你擴(kuò)展抽象事物,定義一個(gè)新的實(shí)現(xiàn)而不需要修改任何客戶端代碼。即”面向接口編程,不要面向?qū)崿F(xiàn)編程“!
三、Liskov’s 替換原則(LSP)
Liskov’s 替換原則意思是:”子類型必須能夠替換它們的基類型。”或者換個(gè)說(shuō)法:”使用基類引用的地方必須能使用繼承類的對(duì)象而不必知道它。” 這個(gè)原則正是保證繼承能夠被正確使用的前提。通常我們都說(shuō),“優(yōu)先使用組合(委托)而不是繼承”或者說(shuō)“只有在確定是 is-a 的關(guān)系時(shí)才能使用繼承”,因?yàn)槔^承經(jīng)常導(dǎo)致”緊耦合“的設(shè)計(jì)。
在基本的面向?qū)ο笤瓌t里,”繼承”通常是”is a”的關(guān)系。如果”Developer” 是一個(gè)”SoftwareProfessional”,那么”Developer”類應(yīng)當(dāng)繼承”SoftwareProfessional”類。在類設(shè)計(jì)中”Is a”關(guān)系非常重要,但它容易沖昏頭腦,導(dǎo)致使用錯(cuò)誤的繼承造成錯(cuò)誤設(shè)計(jì)。
看一個(gè)最最經(jīng)典的例子:
遵循Liskov替換原則的類結(jié)構(gòu)圖。
注:這里,KingFisher(翠鳥)類擴(kuò)展了Bird基類,并繼承了Fly()方法,這沒(méi)有問(wèn)題。
但是下面這個(gè)類結(jié)構(gòu)圖就存在設(shè)計(jì)上的問(wèn)題:
違反Liskov替換原則的類結(jié)構(gòu)圖。
Ostrich(鴕鳥)是一種鳥,這毋庸置疑,并從Bird類繼承,這從概念上說(shuō)沒(méi)有問(wèn)題。但是鴕鳥它能飛嗎?不能,那么這個(gè)設(shè)計(jì)就違反了LSP。因?yàn)樵谑褂肂ird的地方不一定能用Ostrich代替。所以,即使在現(xiàn)實(shí)中看起來(lái)沒(méi)問(wèn)題,在類設(shè)計(jì)中,Ostrich不應(yīng)該從Bird類繼承,這里應(yīng)該從Bird中分離一個(gè)不會(huì)飛的類NoFlyBrid,Ostrich應(yīng)該繼承這個(gè)不會(huì)飛的鳥類NoFlyBrid。
為什么LSP如此重要?
- 如果沒(méi)有LSP,類繼承就會(huì)混亂;如果子類作為一個(gè)參數(shù)傳遞給方法,將會(huì)出現(xiàn)未知行為;
- 如果沒(méi)有LSP,適用與基類的單元測(cè)試將不能成功用于測(cè)試子類;
四、接口分離原則(ISP)
這個(gè)原則的意思是”客戶端不應(yīng)該被迫依賴于它們不用的接口。” 也就是說(shuō),一個(gè)接口或者類應(yīng)該擁有盡可能少的行為(那么,什么叫盡可能少?就是少到恰好能完成它自身的職責(zé)),這也是保證“軟件系統(tǒng)模塊的粒度盡可能少,以達(dá)到高度可重用的目的。
接口包含太多的方法會(huì)降低其可用性,像這種包含了無(wú)用方法的”胖接口”會(huì)增加類之間的耦合。如果一個(gè)類想實(shí)現(xiàn)該接口,那么它需要實(shí)現(xiàn)所有的方法,盡管有些對(duì)它來(lái)說(shuō)可能完全沒(méi)用,所以這樣做會(huì)在系統(tǒng)中引入不必要的復(fù)雜度,降低代碼的可維護(hù)性或魯棒性。
接口分離原則確保實(shí)現(xiàn)的接口有它們共同的職責(zé),它們是明確的,
下面這個(gè)例子充分的說(shuō)明了”接口應(yīng)該僅包含必要的方法,而不該包含其它的“。如果一個(gè)接口包含了過(guò)多的方法,應(yīng)該通過(guò)分離接口將其拆分。
這是一個(gè)違反接口分離原則的胖接口。
注意到IBird接口包含很多鳥類的行為,包括Fly()行為.現(xiàn)在如果一個(gè)Bird類(如Ostrich)實(shí)現(xiàn)了這個(gè)接口,那么它需要實(shí)現(xiàn)不必要的Fly()行為(Ostrich不會(huì)飛)。因此,這個(gè)”胖接口”應(yīng)該拆分成兩個(gè)不同的接口,IBird和IFlyingBird, 而IFlyingBird繼承自IBird。如下圖所示:
這樣的話,重用將變得非常靈活:如果一種鳥不會(huì)飛(如Ostrich),那它實(shí)現(xiàn)IBird接口。如果一種鳥會(huì)飛(如KingFisher),那么它實(shí)現(xiàn)IFlyingBird。
因此,如果我們想要獲得可重用的方案,就應(yīng)當(dāng)遵循接口分離原則,把接口定義成僅包含必要的部分,
五、依賴倒置原則(DIP)
這個(gè)原則的意思是:高層模塊不應(yīng)該依賴底層模塊,兩者都應(yīng)該依賴其抽象。其實(shí)又是”面向接口編程,不要面向?qū)崿F(xiàn)編程“的內(nèi)在要求。
我們考慮一個(gè)現(xiàn)實(shí)中的例子,來(lái)看看依賴倒置原則給我們軟件帶來(lái)的好處。
你的汽車是由很多如引擎,車輪,空調(diào)和其它等部件組成,對(duì)嗎?
注意:這里的 Car 就是高層模塊;它依賴于抽象接口IToyotaEngine 和 IEighteenInchWheel.
而具體的引擎FifteenHundredCCEngine 屬于底層模塊,也依賴于抽象接口IToyotaEngine ;
具體的車輪 EighteenInchWheelWithAlloy同樣屬于底層模塊,也依賴于抽象接口IEighteenInchWheel。
上面Car類有兩個(gè)屬性(引擎和車輪列表),
除SOLID原則外還有很多其它的面向?qū)ο笤瓌t。如:
- “組合替代繼承”:這是說(shuō)相對(duì)于繼承,要更傾向于使用組合;
- “笛米特法則”:這是說(shuō)”你的類對(duì)其它類知道的越少越好”;
- “共同封閉原則”:這是說(shuō)”相關(guān)類應(yīng)該打包在一起”;
- “穩(wěn)定抽象原則”:這是說(shuō)”類越穩(wěn)定,越應(yīng)該由抽象類組成”;