設計模式原則
設計模式原則
參考:https://zhuanlan.zhihu.com/p/54147707
分類
- 開放封閉原則——自己相關的屬性封裝在自己類裡,對外暴露功能
- 職能單一性原則——一個類只負責一個功能領域的相應職責,一個類只負責做一類事
- 里氏替換原則——所有引用基類的地方都能透明地使用子類代替
- 迪米特原則——一個類對於其他類知道的越少越好,就是說一個物件應當對其他物件有儘可能少關聯,降低對其他類的依賴。
- 介面隔離原則——依賴抽象,而不是依賴細節
- 依賴倒置原則——依賴抽象,而不是依賴細節
相關介紹
單一職責原則
- 一個類只負責一個功能領域的相應職責,一個類只負責做一類事
適用場景:
-
當一個類裡需要多個型別分支判斷,影響類的穩定性的時候,需要拆分成多個類
此處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("看家護院"); } } }
-
類裡涉及的職責涉及太多層面,整個類很臃腫,很難理解這個類負責哪些職責
比如銀行客戶端,裡面有貨幣轉換、存錢、取錢等等操作;整個類顯然太龐雜了。我們可以把裡面的業務提取出來,比如提取一個貨幣轉換類專門負責各種貨幣轉換,我們可以抽取一個利率維護類,專門負責管理不同的業務的利率換算;抽取一個業務類,負責存錢、取錢。。。
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) { } }
可違背場景
-
如果型別足夠簡單,可以在類級別去違背單一職責
比如上例裡業務就只有貓和狗兩種型別,就可以寫進一個類裡
-
如果方法足夠簡單,可以在方法級別去違背單一職責
比如餘額查詢,餘額查詢涉及利息計算、賬目顯示;如果利息計算比較簡單,只有一個利率和年限就可計算,這一步就可以寫在在餘額查詢方法裡。
如果利息計算比較複雜(比如要用到個人存款年限、是否為金卡使用者、信譽等級等資料進行復雜計算),則應該把利息計算抽取單獨定義成一個方法
相關建議
- 如果型別複雜了,方法邏輯多了,建議遵循單一職責原則
- 一個方法,不超過50行(編碼建議)
- 一個功能類,不超過300行(編碼建議)
優缺點
優點:
- 降低類的複雜度,類的職責單一,邏輯簡單清晰
- 提高類的可讀性,提高了類的穩定性(變邏輯拆分為新增)。
- 降低了與其他類的耦合性,因為功能單一,修改對其他功能的影響變小
缺點:
- 拆多了會更零碎,不好管理,使用成本高。
拓展
方法級別的單一職責原則:一個方法只做好一件事兒—職責拆分成小方法、接受輸入-業務計算-資料
操作-日誌、分支邏輯拆分--原始碼:父類方法DoS(寫校驗)-DoSCore(核心業務)
類級別的單一/WebCore
專案級別的單一職責原則:專案的職責要清晰—Client、Manager、職責原則:一個類只做好一件事
兒—原始碼:scheme-schemeprovider-handler
類庫級別的單一職責原則:一個類庫做好一件事兒—DAL/BLL/Core/PayCoreBackgroudJob
系統級別的單一職責原則:通用系統拆分---IP庫/日誌中心/線上統計
里氏替換原則
定義一:存在兩個不同類(
T1
、T2
)的物件o1
,o2
,若一個程式裡的所有物件o1
都能使用o2
代替,對程式地行為沒有任何影響,則T2
是T1
的子類
定義二:所有引用基類的地方都能透明地使用子類代替。(透明代表對程式沒有任何影響)
這是與繼承地區別,因為繼承存在重寫,如果存在重寫,這樣替換肯定會有影響的
插播:抽象方法(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)
原則:
- 一個類對於其他類知道的越少越好,就是說一個物件應當對其他物件有儘可能少關聯,降低對其他類的依賴。
- 如果需要建立依賴,儘量把關聯建立在第三方功能實現類裡,不要直接建立聯絡
目的:
- 迪米特法則的初衷在於降低類之間的耦合。避免一個類的更改影響太多其他類。
- 由於耦合度降低,從而提高了類的可複用率和系統的擴充套件性。
- 由於每個類儘量減少對其他類的依賴,但是因為要實現功能類的相互呼叫是很常見的,為了降低更改影響同時實現功能,這個時候建立第三方功能類去間接建立聯絡。如果某一個類有變化,只需要改動這個類和關聯類。
過度使用缺陷:
- 過度使用迪米特法則會使系統產生大量的中介類,從而增加系統的複雜性,使模組之間的通訊效率降低。
結論:
在採用迪米特法則時需要反覆權衡,確保高內聚和低耦合的同時,保證系統的結構清晰。
迪米特法則不希望類之間建立直接的聯絡。
具體應用
參照:.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——書籍
正常的設計是:手機裡面有閱讀軟體,閱讀軟體裡面有書籍
看書的流程是:
- 我想看XXX書(告訴手機我想看書的標題)
- 我進去app根據書籍名稱找這本書,沒有就在app裡下載
- 然後在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)
定義
設計程式碼結構時,高層模組不應該依賴低層模組,二者都應該依賴其抽象。
簡單說:依賴抽象,而不是依賴細節
抽象——抽象類/介面——包含沒有實現的
細節:具體的類——所有的元素都是確定的
要有【面向介面】程式設計的思維
目的
- 減少類與類之間的耦合性,提高系統的穩定性
- 提高程式碼的可讀性和可維護性
- 夠降低修改程式所造成的風險
實踐
-
變數的表面型別儘量是介面或者是抽象類
-
任何類都不應該從具體類派生
-
儘量不要覆寫基類的方法
-
結合里氏替換原則使用
錯誤例子:
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
這個方法依賴於具體的手機型別,比如玩PlayIPhone
、PlayMiPhone
會誕生幾個問題:
如果換了華為榮耀手機,是不是還要在Student類裡新增一個
PlayHonnorPhone
?有一種新的手機機型出現就要修改一次Student類,這合理嗎?從學生玩手機可以看出學生Student是依賴手機這個型別,但是學生一般也就是用手機打電話、上網等,換一個品牌、配置其實實際使用的還是這些功能。也就是說學生依賴的是一個可以打電話、上網、看視訊的東西,這是不是就是一個抽象的手機概念
就如第一條所說,手機實際上就是打電話、上網等功能,除了作業系統、晶片這些少數區別,大多數手機的大部分功能都是一樣的,有必要新出一個手機型別,我就把手機的屬性或者功能重複寫一遍嗎?
正確的例子:
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類很穩定。
總結:
-
層級多了,依賴細節,一個改動會影響一連串---水波效應
-
依賴抽象,類的改動影響就不會擴散
-
相對於細節的多變性,抽象的東西要穩定的多
-
抽象指的是介面或者抽象類,細節就是具體的實現類。
使用介面或者抽象類的目的是制定好規範和契約,而不去涉及任何具體的操作,把展現細節的任務交給他們的實現類去
實踐建議
- 低層模組儘量都要有抽象類或介面,或者兩者都有
- 變數的宣告型別儘量是抽象類或介面(比如玩手機的引數變數)
- 使用繼承時遵循里氏替換原則(避免埋雷)
依賴倒置原則的核心就是要我們面向抽象/介面程式設計
理解了面向抽象/介面程式設計,也就理解了依賴倒置。
現實應用
- 各種工廠的面向抽象---80%的設計模式都跟面向抽象有關
- 現代化開發,IOC已內建—無處不在的IOC---就得有抽象
介面隔離原則(Interface Segregation Principle)
定義
- 客戶端不應該依賴它不需要的介面
- 類間的依賴關係應該建立在最小的介面上
使用規則
-
介面儘量小,但是要有限度。對介面進行細化可以提高程式設計靈活性是事實,但
是如果過小,則會造成介面數量過多,使設計複雜化,所以一定要適度。
-
介面細節要遮蔽(一個功能介面可能有一個序列化的動作,比如獲取食物的介面,介面實現裡需要
備料——烹飪——上菜等諸多步驟但沒必要體現細節)
-
通過介面繼承來組合介面
例子:
有以下幾個類,對應功能如下
手機:打電話 發簡訊 看電影 上網 玩遊戲 拍照 拍視訊 導航 支付
平板: 看電影 上網 玩遊戲 拍照 拍視訊
電視: 看電影 上網 玩遊戲
相機: 拍照 拍視訊
- 將每個功能拆分成一個介面——介面太多不利管理書寫
- 將這些功能統一成一個介面——沒有的功能也得實現(比如電視沒有支付功能)這樣也不合理(違反了定義第一條)
- 介面組合及合併——通過功能的統一性(比如拍照、拍視訊基本上是成對存在)、及統一的目的性(比如提供食物:可以將備料—烹飪—上菜介面組合成一個介面)等邏輯通過介面繼承的方式去組合成新的介面
介面隔離與單一職責的區別:
-
單一職責原則原注重的是職責;而介面隔離原則注重對介面依賴的隔離。
-
單一職責原則主要是約束類,其次才是介面和方法,它針對的是程式中的實現和
細節;而介面隔離原則主要約束介面,主要針對抽象,針對程式整體框架的構建。
開閉原則(Open Closed Principle)
定義
一個軟體實體如類、模組和函式應該對擴充套件開放,對修改關閉。
修改:修改現有程式碼(類-方法)
擴充套件:增加程式碼(類-方法)
為什麼要儘量遵循開閉原則
面嚮物件語言是一種靜態語言,最害怕變化,會波及很多東西,需要全面測試。
如果有變化,最好的方式就是利用新增去替代修改實現新功能,能夠保障原有的功能穩定可靠
其實其他幾個原則也是為了更好實現開閉原則
實現方式
- 新增一個類實現新的方法
- 新增一個類繼承原有的父類,再在類裡定義新的功能