S.O.L.I.D是面向對象設計和編程(OOD&OOP)中幾個重要編碼原則(Programming Priciple)的首字母縮寫。編程
SRP | The Single Responsibility Principle | 單一責任原則 |
OCP | The Open Closed Principle | 開放封閉原則 |
LSP | The Liskov Substitution Principle | 里氏替換原則 |
ISP | The Interface Segregation Principle | 接口分離原則 |
DIP | The Dependency Inversion Principle | 依賴倒置原則 |
1. 單一責任原則(SRP)
當須要修改某個類的時候緣由有且只有一個。換句話說就是讓一個類只作一種類型責任,當這個類須要承當其餘類型的責任的時候,就須要分解這個類。 類被修改的概率很大,所以應該專一於單一的功能。若是你把多個功能放在同一個類中,功能之間就造成了關聯,改變其中一個功能,有可能停止另外一個功能,這時就須要新一輪的測試來避免可能出現的問題,很是耗時耗力。架構
示例:框架
新建一個Rectangle類,該類包含兩個方法,一個用於把矩形繪製在屏幕上,一個方法用於計算矩形的面積。如圖學習
Rectangle類違反了SRP原則。Rectangle類具備兩個職責,若是其中一個改變,會影響到兩個應用程序的變化。測試
一個好的設計是把兩個職責分離出來放在兩個不一樣的類中,這樣任何一個變化都不會影響到其餘的應用程序。優化
2. 開放封閉原則(OCP)
軟件實體應該是可擴展,而不可修改的。也就是說,對擴展是開放的,而對修改是封閉的。這個原則是諸多面向對象編程原則中最抽象、最難理解的一個。this
(1)經過增長代碼來擴展功能,而不是修改已經存在的代碼。
(2)若客戶模塊和服務模塊遵循同一個接口來設計,則客戶模塊能夠不關心服務模塊的類型,服務模塊能夠方便擴展服務(代碼)。
(3)OCP支持替換的服務,而不用修改客戶模塊。編碼
示例:spa
public boolean sendByEmail(String addr, String title, String content) {} public boolean sendBySMS(String addr, String content) {} // 在其它地方調用上述方法發送信息 sendByEmail(addr, title, content); sendBySMS(addr, content);
若是如今又多了一種發送信息的方式,好比能夠經過QQ發送信息,那麼不只須要增長一個方法sendByQQ(),還須要在調用它的地方進行修改,違反了OCP原則,更好的方式是設計
抽象出一個Send接口,裏面有個send()方法,而後讓SendByEmail和SendBySMS去實現它既可。這樣即便多了一個經過QQ發送的請求,那麼只要再添加一個SendByQQ實現類實現Send接口既可。這樣就不須要修改已有的接口定義和已實現類,很好的遵循了OCP原則。
3. 里氏替換原則(LSP)
當一個子類的實例應該可以替換任何其超類的實例時,它們之間才具備is-A關係
客戶模塊不該關心服務模塊的是如何工做的;一樣的接口模塊之間,能夠在不知道服務模塊代碼的狀況下,進行替換。即接口或父類出現的地方,實現接口的類或子類能夠代入。
示例:
public class Rectangle { private double width; private double height; public void setWidth(double value) { this.width = value; } public double getWidth() { return this.width; } public void setHeight(double value) { this.width = value; } public double getHeight() { return this.height; } public double Area() { return this.width * this.height; } } public class Square extends Rectangle { /* 因爲父類Rectangle在設計時沒有考慮未來會被Square繼承,因此父類中字段width和height都被設成private,在子類Square中就只能調用父類的屬性來set/get,具體省略 */ } // 測試 void TestRectangle(Rectangle r) { r.Weight = 10; r.Height = 20; Assert.AreEqual(10, r.Weight); Assert.AreEqual(200, r.Area); } // 運行良好 Rectangle r = new Rectangle(); TestRectangle(r); // 如今兩個Assert測試都失敗了 Square s = new Square(); TestRectangle(s);
LSP讓咱們得出一個很是重要的結論:一個模型,若是孤立地看,並不具備真正意義上的有效性,模型的有效性只能經過它的客戶程序來表現。例如孤立地看Rectangle和Squre,它們時自相容的、有效的;但從對基類Rectangle作了合理假設的客戶程序TestRectangle(Rectangle r)看,這個模型就有問題了。在考慮一個特定設計是否恰當時,不能徹底孤立地來看這個解決方案,必需要根據該設計的使用者所做出的合理假設來審視它。
目前也有一些技術能夠支持咱們將合理假設明確化,例如測試驅動開發(Test-Driven Development,TDD)和基於契約設計(Design by Contract,DBC)。可是有誰知道設計的使用者會做出什麼樣的合理假設呢?大多數這樣的假設都很難預料。若是咱們預測全部的假設的話,咱們設計的 系統可能也會充滿沒必要要的複雜性。推薦的作法是:只預測那些最明顯的違反LSP的狀況,而推遲對全部其餘假設的預測,直到出現相關的脆弱性的臭味(Bad Smell)時,纔去處理它們。我以爲這句話還不夠直白,Martin Fowler的《Refactoring》一書中「Refused Bequest」(拒收的遺贈)描 述的更詳盡:子類繼承父類的methods和data,但子類僅僅只須要父類的部分Methods或data,而不是所有methods和data;當這 種狀況出現時,就意味這咱們的繼承體系出現了問題。例如上面的Rectangle和Square,Square自己長和寬相等,幾何學中用邊長來表示邊, 而Rectangle長和寬之分,直觀地看,Square已經Refused了Rectangle的Bequest,讓Square繼承 Rectangle是一個不合理的設計。
如今再回到面向對象的基本概念上,子類繼承父類表達的是一種IS-A關係,IS-A關係這種用法被認爲是面向對象分析(OOA)基本技術之一。但正方形的 的確確是一個長方形啊,難道它們之間不存在IS-A關係?關於這一點,《Java與模式》一書中的解釋是:咱們設計繼承體系時,子類應該是可替代的父類的,是可替代關係,而不只僅是IS-A的關係;而PPP一書中的解釋是:從行爲方式的角度來看,Square不是Rectangle,對象的行爲方式纔是軟件真正所關注的問題;LSP清楚地指出,OOD中IS-A關係時就行爲方式而言的,客戶程序是能夠對行爲方式進行合理假設的。其實兩者表達的是同一個意思。
4. 接口分離原則(ISP)
不能強迫用戶去依賴那些他們不使用的接口。換句話說,使用多個專門的接口比使用單一的總接口總要好。
客戶模塊不該該依賴大的接口,應該裁減爲小的接口給客戶模塊使用,以減小依賴性。如Java中一個類實現多個接口,不一樣的接口給不用的客戶模塊使用,而不是提供給客戶模塊一個大的接口。
示例:
public interface Animal { public void eat(); // 吃 public void sleep(); // 睡 public void crawl(); // 爬 public void run(); // 跑 } public class Snake implements Animal { public void eat() { } public void sleep() { } public void crawl() { } public void run() { } } public class Rabit implements Animal { public void eat() { } public void sleep() { } public void crawl() { } public void run() { } } }
上面的例子,Snake並無run的行爲而Rabbit並無crawl的行爲,而這裏它們卻必須實現這樣沒必要要的方法,更好的方法是crawl()和run()單獨做爲一個接口,這須要根據實際狀況進行調整,反正不要把什麼功能都放在一個大的接口裏,而這些功能並非每一個繼承該接口的類都所必須的。
5. 依賴注入或倒置原則(DIP)
1. 高層模塊不該該依賴於低層模塊,兩者都應該依賴於抽象
2. 抽象不該該依賴於細節,細節應該依賴於抽象
這個設計原則的亮點在於任何被DI框架注入的類很容易用mock對象進行測試和維護,由於對象建立代碼集中在框架中,客戶端代碼也不混亂。有不少方式能夠實現依賴倒置,好比像AspectJ等的AOP(Aspect Oriented programming)框架使用的字節碼技術,或Spring框架使用的代理等。
(1).高層模塊不要依賴低層模塊;
(2).高層和低層模塊都要依賴於抽象;
(3).抽象不要依賴於具體實現;
(4).具體實現要依賴於抽象;
(5).抽象和接口使模塊之間的依賴分離。
先讓咱們從宏觀上來看下,舉個例子,咱們常常會用到宏觀的一種體系結構模式--layer模式,經過層的概念分解和架構系統,好比常見得三層架構等。那麼依賴關係應該是自上而下,也就是上層模塊依賴於下層模塊,而下層模塊不依賴於上層,以下圖所示。
這應該仍是比較容易理解的,由於越底層的模塊相對就越穩定,改動也相對越少,而越上層跟需求耦合度越高,改動也會越頻繁,因此自上而下的依賴關係使上層發生變動時,不會影響到下層,下降變動帶來的風險,保證系統的穩定。
上面是立足在總體架構層的基礎上的結果,再換個角度,從細節上再分析一下,這裏咱們暫時只關注UI和Service間的關係,以下面這樣的依賴關係會有什麼樣的問題?
第一,當須要追加提供一種新的Service時,咱們不得不對UI層進行改動,增長了額外的工做。
第二,這種改動可能會影響到UI,帶來風險。
第三,改動後,UI層和Logic層都必須從新再作Unit testing。
那麼具體怎麼優化依賴關係才能讓模塊或層間的耦合更低呢?想一想前面講的OCP原則吧,觀點是相似的。
咱們能夠爲Service追加一個抽象層,上層UI不依賴於Service的details,UI和Service同時依賴於這個Service的抽象層。以下圖是咱們的改進後的結果。
這樣改進後會有什麼好處呢?
第一,Service進行擴展時,通常狀況下不會影響到UI層,UI不須要改動。
第二,Service進行擴展時,UI層不須要再作Unit testing。
總結:
這幾條原則是很是基礎並且重要的面向對象設計原則。正是因爲這些原則的基礎性,理解、融匯貫通這些原則須要很多的經驗和知識的積累。舉的例子可能不太貼切也不太準確,反正理解了就行,之後去公司實習什麼的必定要遵循這些原則,不能讓本身寫的代碼讓別人批的一無可取而後胎死腹中,固然還有其餘的一些很重要的原則,我會在後面的時間裏繼續學習和分享!