為什么要有設計原則,我覺得一張圖片就可以解釋這一切
一、單一職責原則(SRP)
對於一個類而言,應該只有一個發生變化的原因。(單一職責不僅僅是指類)
如果一個模塊需要修改,它肯定是有原因的,除此原因之外,如果遇到了其他情況,還需要對此模塊做出修改的話,那么就說這個模塊就兼具多個職責。舉個栗子:
此時我們有個動物類Animal,有個Move()會移動的方法
public class Animal { //動物移動的方法 public void Move(String name) { Console.WriteLine($"動物{name}跑"); } } class Program { static void Main(string[] args) { Animal a = new Animal(); a.Move("狗"); Console.ReadKey(); } }
此時如果傳入一個魚進去就不太合適了,因為魚是不會跑只會游的
a.Move("魚");
此時我們需要兼顧兩個職責,第一個就是普通動物移動的方法,第二個就是魚類的移動方法。我們修改一下,讓這一切變得合理一些
第一種
public class Animal { //動物移動的方法 public void Move(String name) { if (name == "狗") { Console.WriteLine($"動物{name}跑"); } else if (name=="魚"){ Console.WriteLine($"動物{name}游"); } } }
這種的話其實就是讓Move方法兼具普通動物和魚類移動兩個職責(如果你的設計之初就是讓Move滿足所有動物的移動,此時Move方法還是兼具一個職責)
第二種
public class Animal { //普通動物移動的方法 public void RunMove(String name) { Console.WriteLine($"動物{name}跑"); } //魚類移動的方法 public void FishMove(String name) { Console.WriteLine($"動物{name}游"); } }
此時RunMove和FishMove方法的職責是單一的,只管普通動物和魚類的移動,但是Animal類確是兼具了普通動物和魚類移動兩個職責(如果你的設計之初就是讓Animal類滿足所有動物的移動,此時Animal還是兼具一個職責)
第三種
public class RunAnimal { //普通動物移動的方法 public void Move(String name) { Console.WriteLine($"動物{name}跑"); } } public class FishAnimal { //魚類移動的方法 public void Move(String name) { Console.WriteLine($"動物{name}游"); } } class Program { static void Main(string[] args) { RunAnimal a = new RunAnimal(); a.Move("狗"); FishAnimal f = new FishAnimal(); f.Move("魚"); Console.ReadKey(); } }
此時的話RunAnimal類、FishAnimal類和Move方法的職責都是單一的,只做一件事。就拿RunAnimal的Move方法來說,只有普通動物的移動需要做出改變了,才會對Move方法做出修改。
單一職責原則的優點就是高內聚,使得模塊看起來有目的性,結構簡單,修改當前模塊對於其他模塊的影響很低。缺點就是如果過度的單一,過度的細分,就會產生出很多模塊,無形之中增加了系統的復雜程度。
二、開閉原則(OCP)
軟件中的對象(類,模塊,函數等等)應該對於擴展時開放的,但是對於修改是封閉的
一個模塊寫好了,但是如果還想要修改功能,不要對模塊本身進行修改,可能會引起很大的連鎖反應,破壞現有的程序,應該通過擴展來進行實現。通過擴展來實現的前提,就是一開始把模塊抽象出來,而抽象出來的東西要能夠預測到足夠多的可能,因為一旦確定后,該抽象就不能在發生改變。舉個栗子:
現在有個Dog類,Food食物類,還有個Feed類 ,根據傳入食物喂養Dog類動物
public class Dog { public void eat(DogFood f) { Console.WriteLine("狗吃" + f.Value); } } public class Feed { //開始喂食 public void StartFeed(List<Dog> d, DogFood f) { foreach (Dog item in d) { item.eat(f); } } } public class DogFood { public String Value { get { return "狗糧"; } } }
如果有一天,我們引入了新的種類Tiger老虎類,和新的食物Meat肉類,此時要修改Feed喂食類了,這就違反了開閉原則,只做擴展不做修改,如果要讓Feed類符合開閉原則,我們需要對Dog類和Food類做出一個抽象,抽象出Eat和Food抽象類或者接口,這里我就抽象出兩個接口IEat和IFood:
//所有需要進食的動物都要實現此接口 public interface IEat { //此時食物也應該使用接口而不是具體類來接收 //否則只能接收單一的食物,增加食物的話還是需要修改 void eat(IFood food); } //所有食物需要實現此接口 public interface IFood { String Value { get; } }
此時,IEat和IFood是被固定死了,不做修改,這就需要設計之初能夠預測到足夠多的可能,如果需要添加新的功能(動物或食物),只需要實現對應的接口就行了。
修改原有Dog類和DogFood類實現對應的接口:
public class Dog:IEat { public void eat(IFood food) { Console.WriteLine("狗吃" + food.Value); } } public class DogFood:IFood { public String Value { get { return "狗糧"; } } }
修改Feed喂食類,使用接口來接收和使用,使其滿足開閉原則:
public class Feed { //使用接口接收,后續可以傳入實現該接口的子類,因為用到了協變,就需要使用IEnumerable來接受 public void StartFeed(IEnumerable<IEat> d, IFood f) { foreach (IEat item in d) { item.eat(f); } } }
這樣的話,如果要添加新的功能,就不需要對Feed進行修改,而是添加新的類:
public class Tiger : IEat { public void eat(IFood food) { Console.WriteLine("老虎吃" + food.Value); } } public class Meat : IFood { public string Value { get { return "肉"; } } }
調用:
static void Main(string[] args) { //喂食 Feed f = new Feed(); //狗 List<Dog> dog = new List<Dog>(){ new Dog(), new Dog() }; //狗的食物 DogFood df = new DogFood(); //開始喂食 f.StartFeed(dog,df); //老虎 List<Tiger> t = new List<Tiger>() { new Tiger() }; //肉 Meat m = new Meat(); //開始喂食 f.StartFeed(t,m); Console.ReadKey(); }
遵循開閉原則的好處是擴展模塊時,無需對已有的模塊進行修改,只需要添加新的模塊就行,避免了程序修改所造成的風險,抽象化就是開閉原則的關鍵。
三、依賴倒置原則(DIP)
依賴倒置原則主程序要依賴於抽象接口,不要依賴於具體實現。高層模塊不應該依賴底層模塊,兩個都應該以來抽象。抽象不應該依賴細節,細節應該依賴抽象。
依賴:依賴其實就是耦合性,如果A類依賴B類,那么當B類消失或修改時,對A類會有很大的影響,可以說是A類的存在完全就是為B類服務,就是說A類依賴B類。
高層模塊不應該依賴底層模塊,兩個都應該依賴抽象:在上一個例子中,作為高層模塊Feed喂食類依賴底層模塊Dog類和DogFood類(高層和底層就是調用時的關系,因為Feed調用Dog,所以Feed是高層模塊),這樣的話Feed喂食類只能給Dog類吃DogFood,如果引進了其他動物,Feed類此時是無法完成喂食工作的。后來對Feed類、Dog類和DogFood類做出了修改,讓Dog類和DogFood類分別依賴IEat和IFood接口,使Feed類依賴於IEat和IFood接口,這樣的話就使得高層模塊(Feed)和底層模塊(Dog、DogFood)都是依賴於接口。
抽象不應該依賴細節,細節應該依賴抽象:抽象就是抽象類或者接口,細節就是實現抽象類或接口的具體類,這句話的意思其實就是,抽象類或者接口不應該依賴具體的實現類,而應該讓具體的實現類去依賴抽象類或者接口。
遵循依賴倒置原則的好處就是降低了模塊和模塊之間的耦合性,降低了修改模塊后對程序所造成的風險。
四、里氏替換原則(LSP)
一個程序中如果使用的是一個父類,那么該程序一定適用於其子類,而且程序察覺不出父類和子類對象的區別。也就是說在程序中,把父類都替換成它的子類,程序的行為沒有任何變化。
其實里氏替換原則是很容易理解的,如果想滿足里氏替換原則,子類繼承父類時,可以有自己的特點,可以重寫父類的方法,但是父類中有的方法,子類必須是完全可以實現的。開閉原則中的例子就是符合里氏替換原則,而關於里氏替換原則的反例也有很多,例如:正方形不是長方形、玩具槍不能殺人、鴕鳥不會飛,這里就拿企鵝不會飛來舉個反例:
Birds鳥類、Sparrow麻雀類,所有的鳥類都具有一個飛行時間
public abstract class Birds { //所有鳥類都應該具有飛行速度 public abstract double FlySpeed(); } //麻雀類 public class Sparrow : Birds { public override double FlySpeed() { return 10.5; } }
此時我們添加一個Penguin企鵝類,因為企鵝也是鳥,所以也應該繼承自Birds鳥類
//企鵝 public class Penguin: Birds { //實現飛的方法 public override double FlySpeed() { return 0; } }
但是由於Penguin並不會飛,所以飛行速度為0,但是也實現了FlySpeed方法,編譯也沒有報錯啊,但是如果此時有一個Fly方法需要根據鳥類的飛行速度來計算飛行300米所需要的時間
public static double Fly(Birds b) { return 300 / b.FlySpeed(); }
那么,將Penguin企鵝類放入時,則會報出異常,因為300/0是不符合邏輯的,也就不滿足里氏替換原則,因為此時作為父類Birds,如果傳入子類Penguin,程序就會出錯。
不滿足里氏替換原則的根本還是Penguin企鵝類並沒有完全繼承Birds鳥類,因為實現不了FlySpeed方法,所以此時解決方案有兩種,
一種就是在Fly方法中進行判斷:
public static double Fly(Birds b) { //如果傳入的類型為鴕鳥,默認返回0 if (b.GetType().Equals(typeof(Penguin))) { return 0; } else { return 300 / b.FlySpeed(); } }
這樣的話就會違反開閉原則,而且更改代碼會非常麻煩,后續添加功能還需修改。
第二種就是Penguin企鵝類不繼承Birds鳥類,因為此時企鵝類繼承鳥類在此案例中就是強行繼承,雖然現實世界中企鵝也是鳥,但是在編程世界中就行不通了。
總結下來實現里氏替換原則的根本就是,不要強行繼承,如果繼承就要完全實現。
五、接口隔離原則(ISP)
客戶端不應該依賴它不需要的接口;一個類對另一個類的依賴應該建立在最小的接口上
滿足接口隔離原則的前提就是,接口不要設計的太過龐大,什么叫龐大呢?比如一個動物接口就非常龐大,因為如果細分的話,就可以分很多種類的動物,此時動物接口就需要考慮率足夠多的情況來保證動物接口后續不被修改,那么一開始設計時,就可以將動物接口根據具體的需求(例如動物是否會飛和游泳)細分為水里游的動物、天上飛的動物和地上跑的動物,如果還是過於龐大,就再細分。
就比如開閉原則中的IEat接口,就滿足了接口隔離原則,該接口只負責吃的接口,所有需要吃的動物都可以實現該接口,而Feed喂食類依賴IEat接口,此時IEat接口也是最小接口。舉個反例:
此時Feed喂食類不在依賴於IEat接口,而是依賴於IAnimal接口,所有動物(Dog、Tiger)都實現IAnimal接口

public interface IAnimal { //所有動物都會吃 void eat(IFood food); //所有動物都會呼吸 void breathe(); //所有動物都會移動 void move(); //........動物的功能肯定不止這么多 } public class Tiger : IAnimal { public void breathe() { Console.WriteLine("老虎會呼吸"); } public void eat(IFood food) { Console.WriteLine("老虎吃" + food.Value); } public void move() { Console.WriteLine("老虎會跑"); } } public class Dog : IAnimal { public void breathe() { Console.WriteLine("狗會呼吸"); } public void eat(IFood food) { Console.WriteLine("狗吃" + food.Value); } public void move() { Console.WriteLine("狗會跑"); } }
那么此時讓Feed喂食類依賴IAnimal
public class Feed { //使用接口接收,后續可以傳入實現該接口的子類 public void StartFeed(IEnumerable<IAnimal> d, IFood f) { foreach (IAnimal item in d) { item.eat(f); } } }
這樣的話就違反了接口隔離原則,因為Feed喂食類只需要調用對象的eat方法,動物的其他的方法都是不調用的,但是卻依賴了IAnimal接口,這樣的話就顯得很臃腫,而且如果以后不傳入動物,該工廠也負責喂養機器人吃電池,是不是依賴IAnimal就不合適了(如果非要讓機器人實現IAnimal接口是可行的,不過這太不合理了)。
但是Feed如果依賴IEat接口,那么只要能吃東西就可以實現IEat接口,只要能吃東西就可以傳入Feed喂食類喂養,此時Feed類依賴的IEat接口為最小接口。當一個類對另一個類的依賴建立在最小接口上時,該類基本上負責調用此接口中的所有內容,不需要接口中有多余的方法。
六、迪米特法則(LoD)(最少知識原則(LKP))
一個對象應當對其他對象有盡可能少的了解,不要和陌生人說話。
首先來說一下什么叫“陌生人”,首先我們有個類A,A本身肯定是A的朋友,A的屬性同樣也是A的朋友,如果A的方法需要參數是B類型的,那么B也是A的朋友,還有A類中直接創造的對象也是A類的朋友,其他類對於A類來說就是“陌生人”。如果想要滿足迪米特法則,就要盡可能少的寫public方法和變量,不需要讓別的對象知道的方法或者字段就不要公開。
其實迪米特法則的目的也是為了減少模塊間的依賴,降低模塊間的耦合性,這樣才能提高代碼的復用率。舉個栗子:
動物園中有很多動物,而管理員需要每天記錄動物的數量
動物和動物園
//動物 public class Animal { } //動物園 public class Zoo { public List<Animal> animals = new List<Animal>(); }
管理員
//管理員 public class ZooMan { //根據動物園檢查動物的數量 public void CheckCount(Zoo z) { //獲取所有的動物 List<Animal> animals = z.animals; //獲取所有動物的數量 int count = animals.Count; //輸出 Console.WriteLine(count); } }
ZooMan管理員與Animal動物類並沒有直接的朋友關系,但是卻發生了依賴關系,這樣的設計顯然違反了迪米特法則。我們應該對此程序進行修改,讓動物園不對外開放animals屬性,將計算動物所有數量的方法交由Zoo動物園來完成,對外提供GetAnimalCount方法獲取數量,使得ZooMan管理員與Animal不產生依賴關系,修改如下:
//動物園 public class Zoo { //私有所有的動物,只有當前動物園可以訪問 private List<Animal> animals = new List<Animal>(); //但是對外提供獲取所有動物數量的方法 public int GetAnimalCount() { return animals.Count; } }
管理員獲取數量只需要 int count = z.GetAnimalCount();就可以做到了。
迪米特法則的雖然可以直接避免ZooMan管理員與Animal動物類產生依賴,但是卻需要Zoo動物園對外提供一個GetAnimalCount方法,如果盲目的追求迪米特法則時,就會產生很對類似於GetAnimalCount這樣的“中間”方法或模塊,來傳遞間接的調用,有可能造成模塊間通訊效率降低,不容易協調。
其實這么多原則,遵循與不遵循對於實現特定的功能沒有絲毫影響,但程序不可能是一成不變的,重要的是后續需要修改或者添加新的模塊時,你的程序能否做到“擁抱變化” ?
如果有錯誤或者疑問歡迎留言