1、前言
最近在搭建項目的的時候才會想設計原則問題,之前也看過設計模式,沒有寫博客很快就忘了也沒有起到什么作用。現在在項目上遇到了你才會發現它的美。博客園也有很多前輩寫的很好,對於我來說好記性不如爛筆頭嘛。別人寫的在好你看了之后終究是別人的。只有自己寫下來會用了才是自己的。
2、定義
個人理解設計原則其實就是一個規范一樣,為啥要用設計原則?就是為了寫出適應變化、提高復用率、可維護性、可擴展性的代碼。在進行設計的時候,我們需要遵循單一職責原則、開閉原則、里氏替代原則、依賴倒置原則、接口隔離原則、合成復用原則和迪米特法則。
3、單一職責原則
自己的事情自己干,一個類只弄它單一職責的模塊。比如說Login類就只負責登錄相關的業務,User類只負責用戶相關的業務。如果說Login里面又夾雜着User的功能這樣User牽連着其他的類是不是Login類也牽連進去了,這樣下來耦合度就很好了。單一職責原則優點就是降低耦合,提高代碼的復用率使得模塊看起來有目的性,結構簡單,修改當前模塊對於其他模塊的影響很低。缺點就是如果過度的單一,過度的細分,就會產生出很多模塊,無形之中增加了系統的復雜程度。
比如Login能登錄 但是里面又寫了個User吃飯的方法,登錄跟吃飯八桿子打不到一塊去。生物分類學是研究生物分類的方法和原理的生物學分支。分類就是遵循分類學原理和方法,對生物的各種類群進行命名和等級划分:界門綱目科屬種一樣。程序里面類也是一樣的。
Login login=new(); login.Sign("登錄"); public class Login { /// <summary> /// 登錄 /// </summary> public void Sign(string name) { Console.WriteLine($"賬號密碼{name}"); } }
現在在弄進去一個Eat(吃飯)login.Sign("吃飯");輸出 賬號密碼吃飯。感覺就不合適了。如果要兼顧兩個職責這時候要把程序做一點改變
第一種:這樣就讓Sign兼顧了兩個職責:登錄跟吃飯
public void Sign2(string name) { if(name=="登錄") Console.WriteLine($"賬號密碼{name}"); else if(name=="吃飯") Console.WriteLine($"用戶{name}"); }
第二種:此時Sign跟Eat職責都是單一的,但是我們設計之初就是Login是用來登錄的
public class Login { /// <summary> /// 登錄 /// </summary> public void Sign(string name) { Console.WriteLine($"賬號密碼{name}"); } /// <summary> /// 吃飯 /// </summary> /// <param name="name"></param> public void Eat(string name) { Console.WriteLine($"用戶{name}"); } public void Sign2(string name) { if(name=="登錄") Console.WriteLine($"賬號密碼{name}"); else if(name=="吃飯") Console.WriteLine($"用戶{name}"); } }
第三種:此時Login 的Sign 跟User的Eat職責都是單一的,只做一件事。
public class Login { /// <summary> /// 登錄 /// </summary> public void Sign(string name) { Console.WriteLine($"賬號密碼{name}"); } } public class User { /// <summary> /// 吃飯 /// </summary> /// <param name="name"></param> public void Eat(string name) { Console.WriteLine($"用戶{name}"); } }
4、開閉原則(OCP)
強調的是:一個軟件實體(指的類、函數、模塊等)應該對擴展開放,對修改關閉。即每次發生變化時,要通過添加新的代碼來增強現有類型的行為,而不是修改原有的代碼。修改本省的代碼破壞現有的程序可能稍微不注意就引起很大的連鎖反應。通過擴展來實現就是本可能出現的結果抽象出來。
栗子:狗的叫聲 Dog(狗)、Show(實現)、Voice(聲音)。根據實現傳入動物的聲音
public class Dog { public void Call(DogVoice dogVoice) { Console.WriteLine($"狗的叫聲是{dogVoice.Value}"); } } public class DogVoice { public string Value { get { return "汪汪汪"; } } } public class Show { public void ShowVoice(Dog dog,DogVoice dogVoice) { dog.Call(dogVoice); } }
調用
Dog d = new Dog(); DogVoice dv = new DogVoice(); Show s = new(); s.ShowVoice(d,dv);
如果有一天我們引入了新的類小貓類Cat跟聲音CatVoice,這個時候就要修改Show 顯示類了,這就違反了開閉原則。如果要符合開閉原則 我們就要對Dog跟Voice類做一個抽象,抽象出Call跟Voice的類或者接口。這里我們抽象兩個接口ICall 跟IVoice
public interface ICall { void Call(IVoice voice); } public interface IVoice { string Value { get; } } public class Gog2 : ICall { public void Call(IVoice voice) { Console.WriteLine($"狗的叫聲是{voice.Value}"); } } public class DogVoice2 : IVoice { public string Value => "汪汪汪"; } public class Show2 { public void ShowVoice(ICall dog, IVoice dogVoice) { dog.Call(dogVoice); } }
調用
ICall d = new Dog2(); IVoice dv = new DogVoice2(); Show2 s = new(); s.ShowVoice(d, dv);
這樣你要新加錨的叫聲不是就不用修改show了;新添加兩個類就可以了Cat跟CatVoice。
public class Cat : ICall { public void Call(IVoice voice) { Console.WriteLine($"貓的叫聲是{voice.Value}"); } } public class CatVoice : IVoice { public string Value => "喵喵喵"; }
調用
ICall d = new Dog2(); IVoice dv = new DogVoice2(); Show2 s = new(); s.ShowVoice(d, dv); ICall c = new Cat(); IVoice v = new CatVoice(); s.ShowVoice(c, v);
5、里氏替換原則(LSP)
一個程序中的子類如果繼承了父類那么他將滿足父類所有的方法與屬性。也就是說在程序中,把父類都替換成它的子類,程序的行為沒有任何變化。子類可以有自己的特點也可以重寫父類的方法,但是父類有的方法子類一定要實現,這里舉一個簡單的列子,父類動物(Adimal),子類 母雞(Hen)
/// <summary> /// 抽象類父類 動物 /// </summary> public abstract class Animal { /// <summary> /// 抽象方法 下蛋量 /// </summary> /// <returns></returns> public abstract double LayEgg(); /// <summary> /// 一年平均每天下蛋 /// </summary> /// <returns></returns> public abstract double LayEggAvg(Animal s); } public class Hen : Animal { public override double LayEgg() { return 100; } public override double LayEggAvg(Animal s) { return 365 / s.LayEgg(); } }
這時候如果在加一個類 羊 Sheep 羊也屬於動物類 但是它不會下蛋呀,這個時候計算平均下蛋量的時候程序就要出錯;0作為被除數程序編譯都通過不了。
public class Sheep : Animal { public override double LayEgg() { return 0; } public override double LayEggAvg(Animal s) { return 365 / s.LayEgg();//0 } }
要運行也可以直接判斷是不是綿羊類。是的話返回0,這樣又不符合開閉原則了呀。因為Sheep就完全沒有繼承Animal類,它實現不了LayEggAve的方法。所以現實中綿羊屬於動物類,但是程序中不屬於,不要強行繼承,如果繼承就要完全實現。
public class Sheep2 : Animal { public override double LayEgg() { return 0; } public override double LayEggAvg(Animal s) { //傳入的類型如果是綿羊類 返回0 if (s.GetType().Equals(typeof(Sheep2))) { return 0; } else { return 365 / s.LayEgg(); } } }
6、合成復用原則
如果程序一個對象A包含了另一個對象B,那么A就可以委托B來使用B的功能。 這里學生表李有班級 我們就可以通過學生直接看到班級的信息。
public class Student { public Class Class { get; set; } } public class Class { public string GetName { get { return "計科一班"; } 、 } }
調用
Student student = new(); var name=student.Class.GetName;
7、接口隔離原則
原則上使用多個接口,應用程序端不依賴不用多余的接口,這樣也可以保證程序的安全性。比如說系統內部用的接口我們都要實現增刪查改,而對於第三方我們只提供查詢的接口就可以了。
public interface ICardServiceOut { void Query();//查 } /// <summary> /// 系統內部就繼承兩個接口 /// </summary> public class OurCard : ICardService, ICardServiceOut { public void Add() { throw new NotImplementedException(); } public void Query() { throw new NotImplementedException(); } public void Remove() { throw new NotImplementedException(); } public void Update() { throw new NotImplementedException(); } } /// <summary> /// 第三方就只能繼承外部接口 只能查詢操作 /// </summary> public class ThirdCard : ICardServiceOut { public void Query() { throw new NotImplementedException(); } }
8、依賴倒置原則(DIP)
抽象不應該依賴細節,而細節應該依賴抽象,高層模塊不依賴於低層模塊的實現,而低層模塊依賴於高層模塊定義的接口。一般來講,就是高層模塊定義接口,低層模塊負責具體的實現。針對接口編程而不是針對細節編程。詳情查看Asp.Net Core 3.1學習-依賴注入、服務生命周期(6)
9、迪米特法則(LoD)(最少知識原則(LKP))
指的是一個對象應當對其他對象有盡可能少的了解。也就是說,一個模塊或對象應盡量少的與其他實體之間發生相互作用,使得系統功能模塊相對獨立,這樣當一個模塊修改時,影響的模塊就會越少,擴展起來更加容易。如果想要滿足迪米特法則,就要盡可能少的寫public方法和變量,不需要讓別的對象知道的方法或者字段就不要公開,好朋友之間都有自己的秘密一個的意思。
其實用不用設計原則程序都能運行出來,沒有多大影響,但是后面要增加模塊或者修改功能的時候就看你的程序能不能經得起折騰咯?
PS:學習向日葵,做一個積極吸收正能量的人。人生多數時候都是自尋煩惱。就是吸收的負能量太多。要學習向日葵,哪里有陽光就朝向哪里。多接觸優秀的人,多談論健康向上的話題,多想想有利於人生發展的問題。心里若是充滿陽光,人生即便下雨,也會變成春雨。