1. 程式人生 > >面向物件設計的SOLID原則

面向物件設計的SOLID原則

S.O.L.I.D是面向物件設計和程式設計(OOD&OOP)中幾個重要編碼原則(Programming Priciple)的首字母縮寫。

單一責任原則: 
當需要修改某個類的時候原因有且只有一個(THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE)。換句話說就是讓一個類只做一種型別責任,當這個類需要承當其他型別的責任的時候,就需要分解這個類。 
 SingleResponsibilityPrinciple2_71060858

 類被修改的機率很大,因此應該專注於單一的功能。如果你把多個功能放在同一個類中,功能之間就形成了關聯,改變其中一個功能,有可能中止另一個功能,這時就需要新一輪的測試來避免可能出現的問題,非常耗時耗力。

示例:

新建一個Rectangle類,該類包含兩個方法,一個用於把矩形繪製在螢幕上,一個方法用於計算矩形的面積。如圖

 

Rectangle類違反了SRP原則。Rectangle類具有兩個職責,如果其中一個改變,會影響到兩個應用程式的變化。

一個好的設計是把兩個職責分離出來放在兩個不同的類中,這樣任何一個變化都不會影響到其他的應用程式。


開放封閉原則 
軟體實體應該是可擴充套件,而不可修改的。也就是說,對擴充套件是開放的,而對修改是封閉的。這個原則是諸多面向物件程式設計原則中最抽象、最難理解的一個。

OpenClosedPrinciple2_2C596E17 

(1)通過增加程式碼來擴充套件功能,而不是修改已經存在的程式碼。
(2)若客戶模組和服務模組遵循同一個介面來設計,則客戶模組可以不關心服務模組的型別,服務模組可以方便擴充套件服務(程式碼)。


(3)OCP支援替換的服務,而不用修改客戶模組。

示例:

複製程式碼
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原則。


里氏替換原則 
當一個子類的例項應該能夠替換任何其超類的例項時,它們之間才具有is-A關係 
LiskovSubtitutionPrinciple_52BB5162  

客戶模組不應關心服務模組的是如何工作的;同樣的介面模組之間,可以在不知道服務模組程式碼的情況下,進行替換。即介面或父類出現的地方,實現介面的類或子類可以代入。

示例:

複製程式碼
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關係時就行為方式而言的,客戶程式是可以對行為方式進行合理假設的。其實二者表達的是同一個意思。



依賴倒置原則
1. 高層模組不應該依賴於低層模組,二者都應該依賴於抽象 
2. 抽象不應該依賴於細節,細節應該依賴於抽象 
DependencyInversionPrinciple_0278F9E2

這個設計原則的亮點在於任何被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。


介面分離原則 
不能強迫使用者去依賴那些他們不使用的介面。換句話說,使用多個專門的介面比使用單一的總介面總要好。 

InterfaceSegregationPrinciple_60216468

客戶模組不應該依賴大的介面,應該裁減為小的介面給客戶模組使用,以減少依賴性。如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()單獨作為一個介面,這需要根據實際情況進行調整,反正不要把什麼功能都放在一個大的接口裡,而這些功能並不是每個繼承該介面的類都所必須的。


總結:

這幾條原則是非常基礎而且重要的面向物件設計原則。正是由於這些原則的基礎性,理解、融匯貫通這些原則需要不少的經驗和知識的積累。上述的圖片很好的註釋了這幾條原則。

  1. 一個物件只承擔一種責任,所有服務介面只通過它來執行這種任務。
  2. 程式實體,比如類和物件,向擴充套件行為開放,向修改行為關閉。
  3. 子類應該可以用來替代它所繼承的類。
  4. 一個類對另一個類的依賴應該限制在最小化的介面上。
  5. 依賴抽象層(介面),而不是具體類。