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

>>>  技術話題—商業文明的嶄新時代  >>> 簡體     傳統

     昨天Amazon在我們學校有場講座,主要是跟我們分享一些經驗:作為一個程序員該有的編碼硬技術和工作軟技術。當然他們也有來我們學校招聘人才的目的,想要吸引我們去他們公司,于我而言,這么牛逼的一個公司我當然特別想去啦,這場講座我是必聽無疑,的確學到了一些知識。沒有過實際項目的在校生往往不能寫出漂亮的代碼(包括編碼規范、注釋、設計模式、性能等等),因為他們更多注重的正確性,只要結果出來了且是正確的,那么就是好代碼,孰不知這樣的代碼拿給公司的項目經理看,他們會是多么的痛苦啊!呵呵,不說了,作為在校生,我首先得學習編碼規范,這個每個公司有每個公司自己的一套規則,就不說了;其次是代碼的設計模式,這個太大了,需要不斷學習和積累。眾所周知,Java編程最基本的原則就是要追求高內聚和低耦合的解決方案和代碼模塊設計,在這里我先學習下講座上講的程序員該有的藝術氣質—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)
軟件實體應該是可擴展,而不可修改的。也就是說,對擴展是開放的,而對修改是封閉的。這個原則是諸多面向對象編程原則中最抽象、最難理解的一個。

(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原則,更好的方式是

復制代碼
public boolean send(int type, String addr, String title, String content) {

    if(type == 0) { 
        // 通過所有方式發送
    }

    if(type == 1) { 
        // 通過Email發送
    }

    if(type == 2) { 
        // 通過...發送

    ...

    }
}

// 在其它地方調用上述方法發送信息
send(0, addr, title, content);
復制代碼

 

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。
 

這幾條原則是非常基礎而且重要的面向對象設計原則。正是由于這些原則的基礎性,理解、融匯貫通這些原則需要不少的經驗和知識的積累。


網載 2013-06-09 20:04:56

[新一篇] 如何向App Store提交應用

[舊一篇] 三種形式的懶惰
回頂部
寫評論


評論集


暫無評論。

稱謂:

内容:

驗證:


返回列表