SOLID五大設計原則

Agile Principles, Patterns, and Practices in C#

Agile Principles, Patterns, and Practices in C#
by Micah Martin; Robert C. Martin

前段時間聽同事說,傳言面試者如果知道SOLID五大原則,工資可以翻一倍。我趕緊跑去查了查這五大原則,決定背下來!哈哈,開個玩笑,其實很多面試官可能自己都不知道這五大原則,而且即使知道這些原則,在開發過程中嚴格按照這些原則寫的人就更少了。我只覺得這個傳言說明這五大原則很重要,它們是我們能夠寫出更clean更易擴展和更易維護的代碼的基礎。

廢話少說,先列出這五大原則:
S:The Single-Responsibility Principle (SRP)
O:The Open/Closed Principle (OCP)
L:The Liskov Substitution Principle (LSP)
I:The Interface Segregation Principle (ISP)
D:The Dependency-Inversion Principle (DIP)

單一職責原則 The Single-Responsibility Principle (SRP)

The Single-Responsibility Principle: A class should have only one reason to change.

單一職責原則,就是說一個類僅有一個引起它變化的原因。雖然這一原則明確是在說類的設計,但是實際中在一個模塊或者一個方法上同樣適用。

例如,我們有一個Rectangle類,它有兩個方法,其中一個是把矩形畫在屏幕上,另一個則是計算它的面積。有兩個不同的應用會用到Rectangle類,其中一個是用來做幾何計算的,它需要知道矩形的面積,但是不會需要畫出它。另一個應用則是繪圖,它需要把矩形畫出來。那么上面設計的Rectangle類就違反了單一職責的原則(Violates SRP)。

違反這一原則會有什么問題呢?第一,單純做計算的應用程序并沒有用到任何GUI的東西,但是由于它用到了Rectangle,所以需要引用GUI。既然被引用了,那么GUI就需要跟隨計算應用程序被編譯和部署。第二,如果繪圖應用程序的變化需要引起Rectangle的改變,那么計算應用程序也需要重新編譯、測試和部署,否則可能引起不可預測的問題。

也有人把它解釋為只做一件事情,雖然也說得通,但是這并不是作者的本意。這里的職責并不是負責的事情,而是‘A reason for change’。
An axis of change is an axis of change only if the changes occur. It is not wise to apply SRP—or any other principle, for that matter—if there is no symptom。也就是說不要過度解讀這個原則,一個類也可以做不止一件事情,只要讓它改變的原因只有一個就好。

例如,下面是Modem接口,它是否違反SRP就看你怎么用它了。

public interface Modem
{
  public void Dial(string pno);
  public void Hangup();
  public void Send(char c);
  public char Recv();
}

如果從所做的事情來看的話,這里Modem做了兩件事:1)管理連接,DialHangup方法;2)數據通訊,SendRecv方法。
但是這兩個職責應該被分到兩個類中嗎?那就取決于應用系統需要如何改變了。如果應用系統不會對這兩個職責做不同的改變,那也就不需要對它們進行拆分了。

開放閉合原則 The Open/Closed Principle (OCP)

The Open/Closed Principle (OCP): Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

所有的系統在其生命周期里都會改變。如果我們期望自己的系統能夠活過第一個版本,那么開發的時候一定要謹記這句話。需求會變是正常的,好的系統不會拒絕變化,只會需要添加code或者修改很少的code就能支持這些變化。

開放閉合原則同樣適用于類、模塊和方法等,它強調對擴展開放,對修改閉合。看起來說了兩點實際上就是一點:為了適應新的需求,盡量不要修改原始代碼,而是擴展原有的代碼。

要遵循這一原則,最好的辦法就是抽象(abstract),最常用的設計模式方法有策略模式(Strategy)和模板方法(Template Method)。例如,Client類依賴于Server類,但是一旦將來換一個Server,則Client里面涉及到Server的地方全部要改變。這個時候就可以用到Strategy。抽象一個ClientInterfaceClient依賴于這個抽象,對于這個抽象的實現則可以是不同的Server對應不同的策略。與Strategy類似,Template Method也是需要一個抽象,然后在這個抽象的基礎上派生出一些類來做具體的實現。不同的是模板方法在這個抽象中定義了一個抽象方法和一個模板業務方法,模板業務方法會調用抽象方法。抽象方法的具體實現在繼承這個抽象類的具體子類中。

下面用一個常用于說明多態的Shape來舉例說明OCP。
我們目前有兩種類型的Shape,分別是CircleSquare,現在需要一個方法來畫出所有的圖形。下面是一個違反OCP的實現。

//shape.h
enum ShapeType {circle, square};

struct Shape
{
  ShapeType itsType;
};
//circle.h
struct Circle
{
  ShapeType itsType;
  double itsRadius;
  Point itsCenter;
};

void DrawCircle(struct Circle*);

//square.h
struct Square
{
  ShapeType itsType;
  double itsSide;
  Point itsTopLeft;
};

void DrawSquare(struct Square*);

//drawAllShapes.cc
typedef struct Shape *ShapePointer;

void DrawAllShapes(ShapePointer list[], int n)
{
  int i;
  for (i=0; i<n; i++)
  {
    struct Shape* s = list[i];
    switch (s->itsType)
    {
    case square:
      DrawSquare((struct Square*)s);
    break;

    case circle:
      DrawCircle((struct Circle*)s);
    break;
    }
  }
}

為什么說它違反了OCP呢?因為如果我們要再添加一個新的Shape,則需要修改DrawAllShapesShapeType,以及重新編譯所有引用ShapeType的地方。如果我們按照上述的抽象思想去寫Shape,那么可以做到新增一種Shape時只需要新加一個Shape的派生類即可。

public interface Shape
{
  void Draw();
}

public class Square : Shape
{
  public void Draw()
  {
    //draw a square
  }
}

public class Circle : Shape
{
  public void Draw()
  {
    //draw a circle
  }
}

  public void DrawAllShapes(IList shapes)
  {
    foreach(Shape shape in shapes)
      shape.Draw();
  }

There is no model that is natural to all contexts!當然,也會有一些新的需求對上述系統依舊需要做一些修改,比如說,我們現在要求在輸出所有的Shape時按照先SquareCircle的順序畫出來。Robert C. Martin很喜歡一句話,“Fool me once, shame on you. Fool me twice, shame on me.”。面對一些我們不得不對老的code做出一些修改的時候,我們除了痛罵怎么會有這么變態的需求的同時,可以多想想,如果再來一些這么變態的需求,我現在的這種修改方式可以支持嗎?

比如說,對于上面的例子,我們現在要求先畫Square再畫Circle。你可能覺得這個修改很簡單啊,我在DrawAllShapes里面先拿出Square來繪制,再來畫Circle就好了。這就大錯特錯了,又違反了OCP。這樣如果再加入其它的Shape的話,你還是得修改DrawAllShapes。要解決這個問題,還是得抽象。先畫A再畫B,不就是順序的問題嗎?那我們可以讓Shape實現IComparable,這樣畫圖的時候先對這些圖形進行排序不就可以了嗎?

public interface Shape : IComparable
{
  void Draw();
}
public void DrawAllShapes(ArrayList shapes)
{
  shapes.Sort();
  foreach(Shape shape in shapes)
    shape.Draw();
}

不幸的是,這種方法依然違反了OCP,因為在每個Shape的派生類中都要實現CompareTo,而CompareTo肯定需要知道所有的其他子類。

public class Circle : Shape
{
  public int CompareTo(object o)
  {
    if(o is Square)
      return -1;
    else
      return 0;
  }
}

那么,我們就要從Data-Driven Approach的角度來考慮這個問題了,對于這個例子,我們可以用一個table-driven的方法來處理。我們用一個額外的類來實現Shape之間的比較,而只有這里會需要一個Shape所有派生類的列表。這樣新增一個派生類就只需要修改這個列表。另外,為了讓增加Shape的派生類不需要重新編譯舊的代碼,我們可以把ShapeComparer類與Shape相關的模塊分離。

public class ShapeComparer : IComparer
{  
    private static Hashtable priorities = new Hashtable();  
    static ShapeComparer()  
    {    
        priorities.Add(typeof(Circle), 1);    
        priorities.Add(typeof(Square), 2);  
    }  
    private int PriorityFor(Type type)  
    {    
        if(priorities.Contains(type))      
            return (int)priorities[type];    
        else      
            return 0;  
    }  
    public int Compare(object o1, object o2)  
    {    
        int priority1 = PriorityFor(o1.GetType());    
        int priority2 = PriorityFor(o2.GetType());    
        return priority1.CompareTo(priority2);  
    }
}

里氏替換原則 The Liskov Substitution Principle (LSP)

The Liskov Substitution Principle: Subtypes must be substitutable for their base types.

里氏替換原則的內容是,子類型必須能夠替換它的基類型。OCP的實現機制是抽象和多態,而它們的關鍵是繼承。LSP所強調的就是繼承的實現規則。
首先,看一下違反LSP會怎樣?如果違反LSP,類繼承就會混亂,如果子類作為一個參數傳遞給參數為基類的方法,將會出現未知行為;如果違反LSP,適用于基類的單元測試將不能成功用于測試子類。
假如說我們不能保證LSP,即子類不一定能夠替換它的基類,那么我們來看看上一節中關于Shape的例子。如果不保證LSP,那么DrawAllShapes就得按照下面的方式寫,這樣就違反了OCP。所以說,A violation of LSP is a latent violation of OCP。

public static void DrawAllShapes(Shape s)
  {
    if(s.type == ShapeType.square)
      (s as Square).Draw();
    else if(s.type == ShapeType.circle)
      (s as Circle).Draw();
  }

然而,現實中很多違反LSP的情況并不像上例這么明顯。比如說,我們有一個基類是Rectangle,我們在它的基礎上派生出Square。一般說派生類滿足IS-A(Square is a rectangle)就可以,理論上看也確實滿足。但是Rectangle類中有heightwidthSquare類中要求它們必須相等,那么我們可以在Square中對它們的賦值操作進行重寫,即任何對heightwidthset操作都會設置它們倆為同一個值。但是如果用戶有一個下面這樣的方法來使用Rectangle,則這里的設計就違反了LSP。

void g(Rectangle r)
{
  r.Width = 5;
  r.Height = 4;
  if(r.Area() != 20)
    throw new Exception("Bad area!");
}

接口分離原則 The Interface Segregation Principle (ISP)

The Interface Segregation Principle: Clients should not be forced to depend on methods they do not use.

接口分離原則指出,客戶端不應該被迫依賴于它不會用到的方法。
我曾經遇到過這樣一個應用場景,在類P1P2P3中都需要一些config,這些config需要一些其他的操作來獲取,而原本這些config的獲取散落在P1,P2和P3處理邏輯中間。這導致對獲取config相關的代碼改動時會影響到毫不相關的處理邏輯。所以,我們希望把這些config decouple出來,讓P1P2P3依賴于一個抽象的config,而具體獲取config的方法則實現在這個抽象的派生類中。這是下面我們會介紹的另一個原則DIP,但是我要說的是,我當時就試圖寫一個IConfigureP1P2P3中用到的接口全部定義了,然后再對應的去寫幾個Configure類來實現IConfigure的一部分。當時只覺得這樣寫就可以只寫一個接口了,可是它卻需要變得非常“胖”,很顯然違背了ISP。

下面來看一個如何實現接口分離的例子。
假如我們原本有一個Door接口,其中提供了Lock()Unlock()IsOpen()方法。現在我們要新寫一個TimedDoor類,當門打開的時間超過一定的限制就要報警。那么我們可以寫一個TimeClient接口來定義TimeOut的機制,然后為了讓TimedDoor繼承TimeClient,我們讓它的基類Door直接引用TimeClient,如下圖1所示。它顯然違反了ISP,因為并不是所有的Door都會需要TimeClient

圖1. 違反ISP的TimedDoor

一種解決方案是,在TimedDoorTimeClient之間提供一個Adapter,這樣TimedDoor用委托的方式去調用TimeClient,而不需要在它的基類Door上去顯示引用TimeClient。(如圖2所示)

圖2. Separation Through Delegation

另一種解決方案,則是直接讓TimedDoor實現多個接口。(如下圖3所示)

圖3. Separation Through Multiple Inheritance

依賴倒置原則 The Dependency-Inversion Principle (DIP)

The Dependency-Inversion Principle: A). High-level modules should not depend on low-level modules. Both should depend on abstractions. B). Abstractions should not depend upon details. Details should depend upon abstractions.

依賴倒置原則說的是, 高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴于抽象。

為什么要考慮依賴倒置呢?如果我們違反了這一原則,修改低層模塊會影響高層模塊,甚至迫使高層模塊(policy decisions and business models)做出相應的修改。另外,我們在低層模塊上的復用已經做得很好了,但是若高層模塊依賴于低層模塊,那么高層模塊是很難復用的。

我們來看看下面這個例子。如圖4所示,high-level的Policy層依賴于low-level的Mechanism層,而low-level的Mechanism層又依賴于detailed-level的Utility層。這樣由于依賴的傳遞性,就導致了Policy層依賴于Utility層。

圖4. Naive layering scheme

為了去除這種依賴,我們可以在兩層之間引入一個抽象接口,如下圖5所示。這樣Policy層不再依賴于Mechanism層,而是依賴于PolicyServiceInterfaceMechanism層會實現PolicyServiceInterface,那這會不會導致Mechanism層需要依賴Policy層呢?其實不然,PolicyServiceInterface雖然在圖中放在了Policy層,但是它是一個獨立的接口,不涉及任何細節,我們甚至可以把它單獨放進一個package里面。

圖5. Inverted layers

下面再來看一個DIP的例子。
我們有Button類和Lamp類,其中Button感知外部環境,收到Pull消息后決定用戶是否按了這個按鈕;而Lamp則用于影響外部環境,它會接收Turn On/Off消息,根據消息來決定是開燈還是關燈。
下面是一個違反DIP的寫法,這里直接讓Button依賴于Lamp。這樣做的壞處就是Lamp如果有改變,這個Button類也得跟著改變,而且這個Button根本沒法重用,它只能用來控制Lamp的開關。

public class Button
{
  private Lamp lamp;
  public void Poll()
  {
    if (/*some condition*/)
      lamp.TurnOn();
    else lamp.TurnOff();
  }
}

對于這個例子,我們首先需要找到其中的抽象。這里的抽象就是根據用戶的動作來決定狀態on/off。它可以既與Button無關,又與Lamp無關。所以我們可以定義一個ButtonServer(或者考慮到要與Button無關,叫做SwitchableDevice)的接口,這個接口可以讓Button控制來開關某個東西。然后讓Lamp實現這個接口,Button就可以用來控制這個燈了,如果再換成一個東西實現ButtonServerButton依然適用。

圖6. Dependency inversion applied to Lamp

好啦!五大原則我就講完了,工資能不能翻倍就看你們自己了!值得一提的是,光背住上面的條款可沒有用哦,只有真正理解了這樣設計帶來的好處,或者說只有在實踐中違反了上述原則并付出了慘痛的代價,才會對上述原則有深刻的體會。Anyway,首先記住這幾大原則,寫代碼和review別人的代碼時多想想我們有沒有違反這些原則,你一定會有收獲的!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容