程序員該有的藝術氣質—SOLID原則

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。

 

總結:

  1. 一個對象只承擔一種責任,全部服務接口只經過它來執行這種任務。
  2. 程序實體,好比類和對象,向擴展行爲開放,向修改行爲關閉。
  3. 子類應該能夠用來替代它所繼承的類。
  4. 一個類對另外一個類的依賴應該限制在最小化的接口上。
  5. 依賴抽象層(接口),而不是具體類。

      這幾條原則是很是基礎並且重要的面向對象設計原則。正是因爲這些原則的基礎性,理解、融匯貫通這些原則須要很多的經驗和知識的積累。舉的例子可能不太貼切也不太準確,反正理解了就行,之後去公司實習什麼的必定要遵循這些原則,不能讓本身寫的代碼讓別人批的一無可取而後胎死腹中,固然還有其餘的一些很重要的原則,我會在後面的時間裏繼續學習和分享!

相關文章
相關標籤/搜索