1. 程式人生 > 其它 >設計模式原則

設計模式原則

設計模式原則

參考:https://zhuanlan.zhihu.com/p/54147707

分類

  • 開放封閉原則——自己相關的屬性封裝在自己類裡,對外暴露功能
  • 職能單一性原則——一個類只負責一個功能領域的相應職責,一個類只負責做一類事
  • 里氏替換原則——所有引用基類的地方都能透明地使用子類代替
  • 迪米特原則——一個類對於其他類知道的越少越好,就是說一個物件應當對其他物件有儘可能少關聯,降低對其他類的依賴。
  • 介面隔離原則——依賴抽象,而不是依賴細節
  • 依賴倒置原則——依賴抽象,而不是依賴細節

相關介紹

單一職責原則

  • 一個類只負責一個功能領域的相應職責,一個類只負責做一類事

適用場景:

  1. 當一個類裡需要多個型別分支判斷,影響類的穩定性的時候,需要拆分成多個類

    此處Animal就該單獨作為一個類,然後新增Dog,Cat的類繼承Animal

        class Animal
        {
            public string Type { get; set; }
            public void Eat()
            {
                if (Type == "Cat")
                {
                    Console.WriteLine("吃魚");
                }
                else if (Type == "Dig")
                {
                    Console.WriteLine("吃骨頭");
                }
            }
            public void dothing()
            {
                if (Type == "Cat")
                {
                    Console.WriteLine("抓老鼠");
                }
                else if (Type == "Dig")
                {
                    Console.WriteLine("看家護院");
                }
            }
        }
    
  2. 類裡涉及的職責涉及太多層面,整個類很臃腫,很難理解這個類負責哪些職責

    比如銀行客戶端,裡面有貨幣轉換、存錢、取錢等等操作;整個類顯然太龐雜了。我們可以把裡面的業務提取出來,比如提取一個貨幣轉換類專門負責各種貨幣轉換,我們可以抽取一個利率維護類,專門負責管理不同的業務的利率換算;抽取一個業務類,負責存錢、取錢。。。

    class BankClient
    {
        public double TransLateRMBToDollars(int RMBCount)
        {
            Console.WriteLine("人民幣轉美元");
            //double count = func(RMBCount)
            return 0;
        }
        public double TransLateDollarsToRMB(int RMBCount)
        {
            Console.WriteLine("美元轉人民幣");
            //double count = func(RMBCount)
            return 0;
        }
        //存錢
        public void StorageMoney(double money)
        {
    
        }
        //取錢
        public void GetMoney(double money)
        {
    
        }
    
    }
    

可違背場景

  1. 如果型別足夠簡單,可以在類級別去違背單一職責

    比如上例裡業務就只有貓和狗兩種型別,就可以寫進一個類裡

  2. 如果方法足夠簡單,可以在方法級別去違背單一職責

    比如餘額查詢,餘額查詢涉及利息計算、賬目顯示;如果利息計算比較簡單,只有一個利率和年限就可計算,這一步就可以寫在在餘額查詢方法裡。

    如果利息計算比較複雜(比如要用到個人存款年限、是否為金卡使用者、信譽等級等資料進行復雜計算),則應該把利息計算抽取單獨定義成一個方法

相關建議

  1. 如果型別複雜了,方法邏輯多了,建議遵循單一職責原則
  2. 一個方法,不超過50行(編碼建議)
  3. 一個功能類,不超過300行(編碼建議)

優缺點

優點:
  1. 降低類的複雜度,類的職責單一,邏輯簡單清晰
  2. 提高類的可讀性,提高了類的穩定性(變邏輯拆分為新增)。
  3. 降低了與其他類的耦合性,因為功能單一,修改對其他功能的影響變小
缺點:
  1. 拆多了會更零碎,不好管理,使用成本高。

拓展

方法級別的單一職責原則:一個方法只做好一件事兒—職責拆分成小方法、接受輸入-業務計算-資料

操作-日誌、分支邏輯拆分--原始碼:父類方法DoS(寫校驗)-DoSCore(核心業務)

類級別的單一/WebCore

專案級別的單一職責原則:專案的職責要清晰—Client、Manager、職責原則:一個類只做好一件事

兒—原始碼:scheme-schemeprovider-handler

類庫級別的單一職責原則:一個類庫做好一件事兒—DAL/BLL/Core/PayCoreBackgroudJob

系統級別的單一職責原則:通用系統拆分---IP庫/日誌中心/線上統計

里氏替換原則

定義一:存在兩個不同類(T1T2)的物件o1,o2,若一個程式裡的所有物件o1都能使用o2代替,對程式地行為沒有任何影響,則T2T1的子類

定義二:所有引用基類的地方都能透明地使用子類代替。(透明代表對程式沒有任何影響

這是與繼承地區別,因為繼承存在重寫,如果存在重寫,這樣替換肯定會有影響的

插播:抽象方法(abstract)、虛方法(virtual)及介面(interface) - 海岸線summer - 部落格園 (cnblogs.com)

插播:is 與 as 的區別

 is : 相當於判斷,A is B  A是不是B或者A是不是B的子類?

 as :先判斷,在轉換。(它比傳統的強制轉換相對來說要安全一點,因為傳統的強制轉換, 一旦轉換失敗的話,程式就會崩潰,那麼使用as關鍵字,如果轉換不成功,就轉換成空型別)

潛在規則:

  • 儘量將公共屬性、欄位、變數及不涉及重寫的方法抽象到父類
  • 避免出現父類有,子類裡沒有的情況
  • 父類實現的東西,子類就不要再寫了

迪米特法則

迪米特法則(Law of Demeter)又叫作最少知識原則(The Least Knowledge Principle)

原則:

  1. 一個類對於其他類知道的越少越好,就是說一個物件應當對其他物件有儘可能少關聯,降低對其他類的依賴。
  2. 如果需要建立依賴,儘量把關聯建立在第三方功能實現類裡,不要直接建立聯絡

目的:

  1. 迪米特法則的初衷在於降低類之間的耦合。避免一個類的更改影響太多其他類。
  2. 由於耦合度降低,從而提高了類的可複用率和系統的擴充套件性。
  3. 由於每個類儘量減少對其他類的依賴,但是因為要實現功能類的相互呼叫是很常見的,為了降低更改影響同時實現功能,這個時候建立第三方功能類去間接建立聯絡。如果某一個類有變化,只需要改動這個類和關聯類。

過度使用缺陷:

  • 過度使用迪米特法則會使系統產生大量的中介類,從而增加系統的複雜性,使模組之間的通訊效率降低。

結論:

在採用迪米特法則時需要反覆權衡,確保高內聚和低耦合的同時,保證系統的結構清晰。

迪米特法則不希望類之間建立直接的聯絡。

具體應用

參照:.NET(C#) 設計模式六大原則 迪米特法則-CJavaPy

錯誤例子:
    public class LODPatternError
    {
        static void Main(string[] args)
        {
            Phone phone = new Phone();
            phone.read("三國演義");
        }
    }
    class Phone{
        App app = new App();
        
        public void read(string title)
        {
            Book book = new Book(title);
            app.readBook(book);
        }

    }
    class App
    {
        //app裡的書單
        public List<Book> books = new List<Book>();
        public void readBook(Book book)
        {
            if (books.Find(s=>s.Title == book.Title) == null)
            {
                DownloadBook(book);
            }
            Console.WriteLine($"開始讀{book.Title}");
        }
        public void DownloadBook(Book book)
        {
            Console.WriteLine($"開始下載{book.Title}");
            books.Add(book);
        }
    }
    class Book
    {
        public string Title { get; set; }
        public Book(string  title)
        {
            Title = title;
        }
    }

三個類:手機——App——書籍

正常的設計是:手機裡面有閱讀軟體,閱讀軟體裡面有書籍

看書的流程是:

  1. 我想看XXX書(告訴手機我想看書的標題)
  2. 我進去app根據書籍名稱找這本書,沒有就在app裡下載
  3. 然後在app裡閱讀這本書

App相當於手機和書籍類的一個第三方中介類;手機不應該直接和書籍類建立聯絡。

手機的變化不應該對書籍有影響;書籍的變化不應該對手機有影響。顯然在當前手機類的閱讀方法裡就有書籍例項的建立。就是說如果書籍的建立方式變化,我還需要修改手機類裡的這個方法,這顯然是不合理的。

正確例子:

   internal class LODPattern
    {
        static void Main(string[] args)
        {
            Phone phone = new Phone();
            phone.read("三國意義");
        }
    }
    class Phone
    {
        App app = new App();
        public void read(string title)
        {
            app.readBook(title);
        }

    }
    class App
    {
        //app裡的書單
        public List<Book> books = new List<Book>();
        public void readBook(string bookTitle)
        {
            Book book = books.Find(s=>s.Title == bookTitle);
            if (book == null)
            {
                DownloadBook(bookTitle);
            }
            Console.WriteLine($"開始讀{book.Title}");
        }
        public void DownloadBook(string title)
        {
            Console.WriteLine($"開始下載title");
            Console.WriteLine($"下載完成");
            books.Add(new Book(title));
        }

    }
    class Book
    {
        public string Title { get; set; }
        public string Conyent { get; set; }
        public string Author { get; set; }

        public Book(string title)
        {
            Title = title;
        }
    }

依賴倒置原則(Dependence Inversion Principle)

定義

設計程式碼結構時,高層模組不應該依賴低層模組,二者都應該依賴其抽象。

簡單說:依賴抽象,而不是依賴細節

抽象——抽象類/介面——包含沒有實現的

細節:具體的類——所有的元素都是確定的

要有【面向介面】程式設計的思維

目的

  • 減少類與類之間的耦合性,提高系統的穩定性
  • 提高程式碼的可讀性和可維護性
  • 夠降低修改程式所造成的風險

實踐

  1. 變數的表面型別儘量是介面或者是抽象類

  2. 任何類都不應該從具體類派生

  3. 儘量不要覆寫基類的方法

  4. 結合里氏替換原則使用

錯誤例子:

    public class Student
    {
        public string Name { get; set; }
        //玩Iphone手機
        public void PlayIPhone(IPhone phone)
        {
            Console.WriteLine($"這裡是{this.Name} {nameof(phone)}");
            phone.Call();
            phone.Text();
        }
        //玩小米手機
        public void PlayMiPhone(MiPhone phone)
        {
            Console.WriteLine($"這裡是{this.Name} {nameof(phone)}");
            phone.Call();
            phone.Text();
        }
        //那其他手機呢
    }
   public class IPhone
    {
        public string Name { get; set; }
        public  void Call()
        {
            Console.WriteLine("User {0} Call", this.GetType().Name);
        }
        public  void Text()
        {
            Console.WriteLine("User {0} Call", this.GetType().Name);
        }
    }
    public class MiPhone
    {
        public string Name { get; set; }
        public void Call()
        {
            Console.WriteLine("User {0} Call", this.GetType().Name);
        }
        public void Text()
        {
            Console.WriteLine("User {0} Call", this.GetType().Name);
        }
    }

這裡定義幾個類:學生、IPhone手機、小米手機

學生類裡玩手機PlayPhone這個方法依賴於具體的手機型別,比如玩PlayIPhonePlayMiPhone

會誕生幾個問題:

  1. 如果換了華為榮耀手機,是不是還要在Student類裡新增一個PlayHonnorPhone?有一種新的手機機型出現就要修改一次Student類,這合理嗎?

    從學生玩手機可以看出學生Student是依賴手機這個型別,但是學生一般也就是用手機打電話、上網等,換一個品牌、配置其實實際使用的還是這些功能。也就是說學生依賴的是一個可以打電話、上網、看視訊的東西,這是不是就是一個抽象的手機概念

  2. 就如第一條所說,手機實際上就是打電話、上網等功能,除了作業系統、晶片這些少數區別,大多數手機的大部分功能都是一樣的,有必要新出一個手機型別,我就把手機的屬性或者功能重複寫一遍嗎?

正確的例子:

   public class Student
    {
        public string Name { get; set; }
        //玩手機
        public void PlayPhone(AbstractPhone phone)
        {
            Console.WriteLine($"這裡是{this.Name} {nameof(phone)}");
            phone.Call();
            phone.Text();
        }
    }
    public abstract class AbstractPhone
    {
        public string Name { get; set; }
        public abstract void Call();

        public abstract void Text();
    }
    public class MiPhone: AbstractPhone
    {
        public override void Call()
        {
            Console.WriteLine("User {0} Call", this.GetType().Name);
        }
        public override void Text()
        {
            Console.WriteLine("User {0} Call", this.GetType().Name);
        }
    }
    public class IPhone : AbstractPhone
    {
        public override void Call()
        {
            Console.WriteLine("User {0} Call", this.GetType().Name);
        }
        public override void Text()
        {
            Console.WriteLine("User {0} Call", this.GetType().Name);
        }
    }

這樣手機種類的增加不會影響學生類Student的變化,而且抽象手機AbstractPhone傳送變化的情況比具體的手機變化的頻率小太多。所以Student類很穩定。

總結:

  • 層級多了,依賴細節,一個改動會影響一連串---水波效應

  • 依賴抽象,類的改動影響就不會擴散

  • 相對於細節的多變性,抽象的東西要穩定的多

  • 抽象指的是介面或者抽象類,細節就是具體的實現類。

    使用介面或者抽象類的目的是制定好規範和契約,而不去涉及任何具體的操作,把展現細節的任務交給他們的實現類去

實踐建議

  1. 低層模組儘量都要有抽象類或介面,或者兩者都有
  2. 變數的宣告型別儘量是抽象類或介面(比如玩手機的引數變數)
  3. 使用繼承時遵循里氏替換原則(避免埋雷)

依賴倒置原則的核心就是要我們面向抽象/介面程式設計

理解了面向抽象/介面程式設計,也就理解了依賴倒置。

現實應用

  1. 各種工廠的面向抽象---80%的設計模式都跟面向抽象有關
  2. 現代化開發,IOC已內建—無處不在的IOC---就得有抽象

介面隔離原則(Interface Segregation Principle)

定義

  1. 客戶端不應該依賴它不需要的介面
  2. 類間的依賴關係應該建立在最小的介面上

使用規則

  1. 介面儘量小,但是要有限度。對介面進行細化可以提高程式設計靈活性是事實,但

    是如果過小,則會造成介面數量過多,使設計複雜化,所以一定要適度。

  2. 介面細節要遮蔽(一個功能介面可能有一個序列化的動作,比如獲取食物的介面,介面實現裡需要

    備料——烹飪——上菜等諸多步驟但沒必要體現細節)

  3. 通過介面繼承來組合介面

例子:

有以下幾個類,對應功能如下

手機:打電話 發簡訊 看電影 上網 玩遊戲 拍照 拍視訊 導航 支付

平板: 看電影 上網 玩遊戲 拍照 拍視訊

電視: 看電影 上網 玩遊戲

相機: 拍照 拍視訊

  • 將每個功能拆分成一個介面——介面太多不利管理書寫
  • 將這些功能統一成一個介面——沒有的功能也得實現(比如電視沒有支付功能)這樣也不合理(違反了定義第一條)
  • 介面組合及合併——通過功能的統一性(比如拍照、拍視訊基本上是成對存在)、及統一的目的性(比如提供食物:可以將備料—烹飪—上菜介面組合成一個介面)等邏輯通過介面繼承的方式去組合成新的介面

介面隔離與單一職責的區別:

  • 單一職責原則原注重的是職責;而介面隔離原則注重對介面依賴的隔離

  • 單一職責原則主要是約束類,其次才是介面和方法,它針對的是程式中的實現和

    細節;而介面隔離原則主要約束介面,主要針對抽象,針對程式整體框架的構建

開閉原則(Open Closed Principle)

定義

一個軟體實體如類、模組和函式應該對擴充套件開放,對修改關閉。

修改:修改現有程式碼(類-方法)

擴充套件:增加程式碼(類-方法)

為什麼要儘量遵循開閉原則

面嚮物件語言是一種靜態語言,最害怕變化,會波及很多東西,需要全面測試。

如果有變化,最好的方式就是利用新增去替代修改實現新功能,能夠保障原有的功能穩定可靠

其實其他幾個原則也是為了更好實現開閉原則

實現方式

  1. 新增一個類實現新的方法
  2. 新增一個類繼承原有的父類,再在類裡定義新的功能