讓姑姑不再划拳 碼農也要有原則 : SOLID via C#


  “姑娘,別這樣。我們是有原則的。” 

  “一個有原則的程序猿是不會寫出 “摧毀地球” 這樣的程序的,他們會寫一個函數叫 “摧毀行星”而把地球當一個參數傳進去。”

  “對,是時候和那些只會滾鍵盤的麻瓜不同了,我們可是有高逼格的程序猿。”

  [小九的學堂,致力於以平凡的語言描述不平凡的技術。如要轉載,請注明來源:小九的學堂cnblogs.com/xfuture]

 

  寫代碼其實就是給一個世界創造秩序,世界越大就越需要原則,各司其職,統籌合作自然是比無腦的積木堆積星球來的令人信服,部分基於SOLID architecture principles using simple C# examples

  下面來用C#語言展現面向對象最佳實踐的SOLID軟件設計原則

 目錄

 

 何為SOLID?

  S.O.L.I.D.是一組面對面向對象設計的最佳實踐的設計原則。術語來自Robert C.Martin的著作Agile Principles, Patterns, and Practices in C#,代表了下面五個設計原則:

    1. SRP(Single Responsibility Principle) 單一責任原則,

    2. OCP(Open Closed Principle) 開放封閉原則,

    3. LSP(Liskov Substitution Principle) 里氏替換原則,

    4. ISP(Interface Segregation Principle) 接口分離原則,

    5. DIP(Dependency Inversion Principle) 依賴倒置原則,

  下面用C#例子來一一介紹。

 

 S:SRP, Single Responsibility Principle, 單一責任原則

  人類學習和理解最快的方式是實踐,這點在編程上顯得尤為突出。理解SOLID最好的方式就是先去了解它解決了什么問題。

  首先給大家出一道大家來找茬,下面這段代碼中有一個很大的問題,你找到了嗎?(停停停,不用去倒杯茶細細來看,因為這段代碼已經簡單到沒朋友了)

  那我們現在就拋開和華生的基情,對這個"作案現場"來調查一番。

class Customer
    {
        public void Add()
        {
            try
            {
                // Database code goes here
            }
            catch (Exception ex)
            {
                System.IO.File.WriteAllText(@"c:\Error.txt", ex.ToString());
            }
        }
    }

  相信大家都發現這個問題出在哪里了,一個顧客類竟然可以自主寫log!Customer Class 應該是要做關於Customer Datavalidation或者訪問顧客相關的數據庫進行存儲的相關操作,實現Log的記錄實際上已經超出了其責任的范圍。

  這就像小龍女不去做個安靜的美姑姑而去學划拳和人斗酒一樣,WTF!

  

  當明日需要你改造Log記錄的實現或路徑的時候而你卻push了一段Customer類的改動,這會讓人感到非常奇怪的。

  這也讓我想起來了一個世界知名的工具-瑞士軍刀。毫無疑問它很棒,但當你需要改動其中一個部分的時候其余部分要一起重新來排列保證不會互相干擾到。而且你可以嘗試一個場景,用瑞士軍刀掏耳朵,那種感覺真的是醉了。

  

  倒不如我們一一拆分,各司其職,剪子剪紙,耳勺掏耳,使部件功能簡單化,互不影響。這個原則適用於軟件架構中類和對象的設計。

  

  所以,簡而言之,SRP就是指單個類應該有且僅有單個職能。所以我們可以對剛才案例朝這個目標進行初步改造,首先將記錄log的邏輯在一個單獨的FileLogger類上實現:

class FileLogger
    {
        public void Handle(string error)
        {
            System.IO.File.WriteAllText(@"c:\Error.txt", error);
        }
    }

  現在Customer類可以歡快的拋棄“五魁首六六六”,FileLogger class 來負責記錄log的具體實現,而customer class可以更專注的負責自己的模塊。

class Customer
    {
        private FileLogger obj = new FileLogger();
        publicvirtual void Add()
        {
            try
            {
                // Database code goes here
            }
            catch (Exception ex)
            {
                obj.Handle(ex.ToString());
            }
        }
    }

  如果有一些SRP經驗的朋友可能已經發現,其實這種解決方案並不能完全解決SRP的問題。因為try catch其實並不是Customer類需要關心的功能。

  在記錄Log這一層,不同的語言和結構都會有一個類似Asp.Net中Global.asax或者WPF中App.xaml.cs這類文件可以集中來處理這些冒泡的錯誤,這樣Customer類中便不會有TryCatch的方法。

  其實這個程序依然可以更好,也可以有更多的解決方案,但此文旨在使用足夠簡單的例子來用C#闡述SOLID,也希望可以不禁錮大家思維,有好的方案可以在下面回復和交流,來產出一個偉大的解決方案。

  在codeproject里有一個答案是很不錯的,具體實現就不劇透了,如感興趣,可以戳:http://www.codeproject.com/Articles/703634/SOLID-architecture-principles-using-simple-Csharp?msg=4729987#xx4729987xx

 

 O:OCP, Open Closed Principle 開放封閉原則

  上一個“場景”過了SRP階段我們要繼續開始OCP階段了, OCP簡單來說就是 對擴展是開放的,對修改是封閉的。

  在Customer類中我們現在添加一個屬性來表示他是黃金用戶還是銀色用戶。當CustType為1時為Gold用戶,為2時為Silver用戶.根據用戶類型不同來返回不同的折扣。

  來繼續來找茬了,這個節奏好像看起來大家看完本文后能在大家來找茬中無往不勝啊,haha。

  開啟福爾摩斯模式,關注在getDiscount方法中的if語句:

class Customer
{
        private int _CustType;

        public int CustType
        {
            get { return _CustType; }
            set { _CustType = value; }
        }

        public double getDiscount(double TotalSales)
        {
                if (_CustType == 1)
                {
                    return TotalSales - 100;
                }
                else
                {
                    return TotalSales - 50;
                }
        }
}

  “嫌疑人”出現了,當我們再添加一個用戶類型時,我們還需要添加修改if中的折扣邏輯,也就是我們需要修改Customer Class。

  當我們一次次更改Customer Class,我們還需要確認之前的邏輯是沒錯的以及引用該Class的更多的邏輯也是沒問題的,也就說需要一次又一次的測試。

  那么問題來了,挖掘...不對,是如何來避免多次的“Modify”而帶來的惡果呢,那就是“EXTENSION”(擴展).

  當我們每次增加一個用戶類型的時候,我們就增加一個Customer的擴展類,因此我們也就每次只需要測試新加的類。

class Customer
{
        public virtual double getDiscount(double TotalSales)
        {
            return TotalSales;
        }
}

  class SilverCustomer : Customer
    {
        public override double getDiscount(double TotalSales)
        {
            return base.getDiscount(TotalSales) - 50;
        }
    }

class goldCustomer : SilverCustomer
    {
        public override double getDiscount(double TotalSales)
        {
            return base.getDiscount(TotalSales) - 100;
        }
    }

  這樣也就解決了多次修改帶來的問題,通過擴展基類,而不是修改。

  OCP原則 擁抱擴展,拒絕修改,保證了現有邏輯的穩定性。

  

  其實還有一張比較XXX的圖來表示OCP,我這邊就不鑲嵌到文章里了,因為....好奇的小盆友可以戳戳,記得留言寫下感悟...:戳我

 

 L: LSP Liskov Substitution Principle 里氏替換原則

  跨過前兩個坎,現在我們來到了第三個原則,這次我們換一個模型。首先我們有一個Bird的Class,有一個Fly的方法:

class Bird
{
  public void Fly()
  {
    // Fly Logic
  } }

  后來我們發現生物學上企鵝也屬於鳥類,當然這只企鵝不在深圳,但它不會飛。

  

  於是我們設計一個Penguin的class 繼承自Bird 重寫其Fly方法,標明其不會飛

class Penguin: Bird
    {
        public override void Fly()
        {
            throw new Exception("Can Not Fly");
        }
    }

  眨眼一看是沒有問題,但實際上問題可大了去了,於是,

  好吧,大家來找茬又開始了,這次的茬可是隱藏的很深。

  首先在程序開發中,你並不能保證你繼承的父類重寫了的方法是安全的,它里面可能包含其他邏輯。而且在后續的開發中,極容易出現下面的代碼:

List<Bird> Birds = new List<Bird>();
// Add Bird Logic
foreact(var bird in Birds)
{
     bird.Fly();      
}

  這時企鵝君便要崩潰了。風險就在別人使用你設計的類的時候並沒有想到並非所有的子類都符合父類的要求。這便不符合LSP的原則了。

  LSP俗語是:“老鼠的兒子會打洞。”,其實就是子類是和父類有相同的行為和狀態,是可以完全替換父類的。這也是保護了OCP的開放擴展關閉修改的原則。

  

  前面例子更改方法可以采用父類高聚合,子類低耦合的原則來做,父類要精,子類可以采用接口實現的方法來進行擴展。

  比如可以增加一個IFly的接口,實現該接口的子類便可以飛,而父類中便不再包含Fly方法。

  還有其他方法,大家可以擴散思想回復在下面。

 

 I:ISP Interface Segregation Principle 接口分離原則

   還是企鵝和鳥的問題,我們現在有個鳥類接口:

    interface IBird
    {
        void Fly();
        void Eat();

    }

  燕子實現了Fly和Eat,而企鵝只能實現Eat,所以代碼如下:

class Swallow : IBird
    {
        public void Fly()
        {
            // Fly Logic
        }

        public void Eat()
        {
            // Eat
        }
    }

    class Penguin : IBird
    {
        public void Fly()
        {
            //No Implementation
        }
        public void Eat()
        {
            // Eat
        }
    }

  如果我們保持這個設計,在什么場景會出現問題呢?

  好了!5!4!3!2!1!

  揭曉答案!(這個流程會不會太綜藝化..=。=)

  企鵝繼承了IBird的接口,就必須實現其所以方法,雖然它在飛的方法里什么都沒做。負面效果就是在統計會飛的鳥兒的時候,因為企鵝也實現了其Fly方法,但也會被歸納其中。

  其實它壓根不會飛嘛!!!

  

  所以這樣就違反了接口分離規則,接口分離規則旨在使用多個特定功能的接口來避開通用接口造成的富余化。

  也就是說 我們可以分離IBird為IFly和IEat,Swallow實現IFly和IEat,而呆呆的企鵝只需要實現IEat就好。

  這樣讓程序的設計更加靈活,當需求改變的時候,我們要做的工作便小很多也風險少很多,也會發現更多的樂趣。

  

 D:DIP, Dependency Inversion Principle, 依賴倒置原則

 依賴倒置原則在SOLID里是我認為最為出彩的原則和技術。簡單來說就是面向接口編程。

  之前博客中已有文來介紹其所以然,這里引用來:http://www.cnblogs.com/xfuture/p/3682666.html ,下面做一下簡單的介紹,也算是...大片預告片?...

  遠古母系氏族,每個人都是一個獨立的個體,需要什么工具就需要自己去打磨一件工具,自己需要了解所有的流程才能生存。比如打獵,從前期准備繩索,尖木,到中期做陷阱,后期收成,都需要了解的非常透徹。對應編程中便是new 繩索()new 尖木()new 陷阱()new X()。實例化所有需要的資源,然后再進行邏輯流程。

  人類逐漸在進步,工業革命的來襲,改變了整個社會的結構。人再不需要了解所有的流程,只需要去一個工廠或者采購平台,輸入自己想要的東西,便能得到。對應編程中便是工廠模式,需要一個靜態工廠類,一個抽象產品類型的類,一個你想拿到的可以具象化的產品類,從此便進入了全民淘寶年代,需要什么購買什么。

  當你為了修一個頂樓的燈泡購買了梯子,但當修好后,如何處理這個梯子便成了難題,扔掉不舍,不扔去賣二手又很麻煩。這時候就需要我們的主角:和諧社會登場了!主張不鋪張不浪費,這便是一種回收機制,你需要它只需要說一聲,秒秒鍾就到你手里,你也不需要知道他來自哪里。不需要了你也不用管,我直接秒秒鍾再變走。是不是有一種魔術的感覺?這便是依賴注入!依賴注入解除了對象和對象的依賴關系,需要其他對象,會有外部直接注入,而你自己不需要關心他如何而來。對象符合OCP原則(對外擴展開放,對修改關閉), 容器負責所有關系匹配。在此層,容器便是這個社會的規則,而對象只需要關心自己所完成的一部分。輕松愜意!

  

  

 總結

 Shivprasad koirala 用一句話總結的SOLID總結的很好,這里就不翻譯了,大家來感受下大牛對SOLID精確的理解:

 

  S:SRP, A class should take care of only one responsibility.

  O: OCP, Extension should be preferred over modification.

  L: LSP, A parent class object should be able to refer child objects seamlessly during runtime polymorphism.

  I: ISP, Client should not be forced to use a interface if it does not need it.

  D: DIP, High level modules should not depend on low level modules but should depend on abstraction.

  

  呼,希望大家喜歡文章風格,也希望能批評指正。

  另有WPF2000Tips系列,大家感興趣可以點點左邊WPF標簽,里有系列文。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM