C#設計模式:六大原則(上)
面向物件設計原則,是一種指導思想,在程式設計過程中,要儘量的去遵守這些原則,用於解決面向物件設計中的可維護性,可複用性以及可擴充套件性。常用的,就是我們日常所說的6大原則,分別是:單一職責(SRP)、里氏替換原則(LSP)、依賴倒置原則(DIP)、介面隔離原則(ISP)、迪米特法則(LOD)、開閉原則(OCP)。下面就來分別說說這些原則:
一、 單一職責(Single Reponsibility Principle,SRP)
一個類只負責一項職責。換種說法,就一個類而言,應該只有一個引起它變化的原因。
這個原則最簡單,也是備受爭儀,較難運用的一個原則。這個和類的職責有關,主觀性比較強,沒有一個量化的標準,開發設計人員對職責怎麼定義,以及怎麼劃分類的職責,和每個人的分析設計思想及相關實踐經驗,都有比較大的關係。對於一個類而言,不能承擔太多的職責,職責過多,就會耦合在一起,一個職責變化,會影響其它職責的運作。過多的耦合,還會影響其複用性。對於軟體系統而言,小到類的方法,介面的定義,大到模組,類庫,也都是一樣的。 單一職責的指導思想,就是為了實現高內聚,低耦合
。
下面來看一個簡單的例子:以一個建築工為例,這個人比較厲害,泥瓦工,木工,油漆工都能做,程式碼如下:
public class Builder { public void Work() { Console.WriteLine("我開始做泥瓦工的活了"); Console.WriteLine("我開始做木工的活了"); Console.WriteLine("我開始做油漆工的活了"); } }
這個程式碼簡單的不能再簡單了,一看就懂。不管做啥,都整到一個方法裡面,就是個大雜燴,這裡只是顯示,沒什麼邏輯,如果邏輯多的話,只是判斷的話,你就要各種 if else ... 了,自己想想吧.....都不敢想了!那就來優化一下,先看張類圖

圖1.1
一般情況下,都會想到這種方式,分成三個不同的方法來處理,程式碼很簡單,這裡就不貼出來了,但這樣同樣的有問題,一個人(類)做這麼多事情,你不會很“累”嗎?說白點,就是職責太多,即要做泥瓦的活,又要做木工的活,如果哪天趕工,臨時來個只做木工的或其它工種的,你就要改類,木工的程式碼也沒法複用,這就違背了單一職責。因此,需要對類進行拆分,使其滿足單一職責,重構後如圖1.2:

圖1.2 職責分明的建築工人類圖
各做各的,互不影響,就如同現在建築工人分工,做什麼都很明確。引入到軟體設計裡面,類的複雜性降低了,可讀性也同時提高了,最重要的是職責劃分也明確了。當然,也就更容易維護了。
程式碼如下
public interface IBuilder { void Work(); } public class TilerBuilder : IBuilder { public void Work() { Console.WriteLine("我是泥瓦工,開始工作了"); } } public class WoodBuilder : IBuilder { public void Work() { Console.WriteLine("我是木工,開始工作了"); } } public class PaintBuilder : IBuilder { public void Work() { Console.WriteLine("我是泥瓦工,開始工作了"); } }
二、里氏替換原則(Liskov Substitution Principle,LSP)
所有使用基類的地方,都可以使用其子類來代替,而且行為不會有任務變化
面嚮物件語言的繼承是項很牛的設計,普通類間父子繼承,抽象類以及介面,它們之間的相互關聯與糾纏,看似複雜,實則給我們帶來很多好處:程式碼共享,減少建立類的工作量,提高了程式碼的複用性;提高了程式碼的可擴充套件性與專案的開放性,實現父類方法後,子類可任意擴充套件,想想一些框架的擴充套件介面不都是通過繼承來完成的麼。 里氏替換原則就是為良好的繼承定義了一個規範
。主要如下:
- 子類必須完全實現父類的屬性和方法,如果子類不擁有父類的全部屬性或者行為,不能強行繼承,要斷掉繼承。
- 子類可以擁有父類沒有的屬性或者方法,子類出現的地方,父類不能代替。
一直在糾結舉個什麼例子,還是拿鳥來說事吧,通俗易懂。先看個反例,鳥類都需要吃東西,都需要喝水,還可以飛,程式碼如下:
public class Bird { public string Name => this.GetType().Name; public void Eat() { Console.WriteLine($"我是{this.Name},我需要吃東西"); } public void Drink() { Console.WriteLine($"我是{this.Name},我需要喝水"); } public void Fly() { Console.WriteLine($"我是{this.Name},我可以飛"); } } /// <summary> /// 現在來了只比較大的鳥,叫鴕鳥,繼承了鳥類 /// </summary> public class Ostrich : Bird { //Do nothing }
呼叫一下
class Program { static void Main(string[] args) { try { Bird bird = new Ostrich(); bird.Eat(); bird.Drink(); bird.Fly(); } catch (Exception e) { Console.WriteLine(e.Message); } Console.Read(); } }
執行結果:
我是Ostrich,我需要吃東西
我是Ostrich,我需要喝水
我是Ostrich,我可以飛
是不是出問題了,鴕鳥顯然是不能飛的,也繼承了鳥類,這就違背了里氏替換原則。鴕鳥雖然是鳥類,可不能飛,比較特珠,就需要斷掉繼承。鴕鳥這是說話了,我不能飛,我也要吃和喝啊,怎麼辦?那你都不屬於動物嗎,我們來使用里氏替換原則重構一下:

圖2.1 重構後的類圖
類圖不復雜,很容易理解,抽出一個共同的基類動物,然後繼承各自的功能,互不影響,根據需求還可以有自己的方法,孔雀可以開屏了。
到這裡,你是不是看出點什麼問題了,如果父類有什麼改動或需要去除一個方法什麼的,這就麻煩了,這就是里氏替換的一個缺陷了:繼承是侵入式的,程式碼靈活性受到限制,增強了耦合性。
程式碼如下:
public class Animal { public string Name => this.GetType().Name; public void Eat() { Console.WriteLine($"我是{this.Name},我需要吃東西"); } public void Drink() { Console.WriteLine($"我是{this.Name},我需要喝水"); } } public class Bird : Animal { /// <summary> /// 鳥有自己可以飛的方法 /// </summary> public void Fly() { Console.WriteLine($"我是{this.Name},我可以飛"); } } public class Ostrich : Animal { //do nothing } public class Sparrow : Bird { //do nothing } public class Peacock : Bird { /// <summary> /// 孔雀可以開屏 /// </summary> public void Open() { Console.WriteLine($"我是{this.Name},我要開屏了,我不是老孔雀"); } }
呼叫如下
class Program { static void Main(string[] args) { try { { Bird bird = new Sparrow(); bird.Fly();//可以飛 } { //Bird bird = new Peacock(); //子類出現的地方父類不能代替 Peacock bird = new Peacock(); bird.Fly(); bird.Open(); } } catch (Exception e) { Console.WriteLine(e.Message); } Console.Read(); } }
三、依賴倒置原則(Dependence Inversion Principle,DIP)
高層模組不應該依賴低層模組,兩者都應該依賴其抽象,不要依賴細節
在C#中,抽象就是指介面或者抽象類,兩者都不能直接進行例項化;細節就是實現類,就是實現了介面或繼承了抽象類而產生的類就是實現類,可以直接被例項化。所謂的高層與低層,每個邏輯實現都是由原始邏輯組成,原始邏輯就屬於低層模組,像我們常說的三層架構,業務邏輯層相對資料層,資料層就屬於低層模組,業務邏輯層就屬於高層模組,是相對來說的。 依賴倒置原則就是程式邏輯在傳遞引數或關聯關係時,儘量引用高層次的抽象,不使用具體的類,即是使用介面或抽象類來引用引數,宣告變數以及處理方法返回值等
。這樣就要求具體的類就儘量不要有多餘的方法,否則就呼叫不到。說簡單點,就是“面向介面程式設計”。
現在學車很流行,駕校也很多(學習的車是真心的破舊),我當時都是些老捷達,皇冠之類的,根據依賴倒置的原則,我們來實現下這個過程,如圖3.1

圖3.1 依賴倒置原則的類圖
一個學生的抽象類,一個汽車的介面,分別定義了各自的職能,具體程式碼如下:
public interface ICar { /// <summary> /// 汽車是可以開動的 /// </summary> void Run(); } public class Jetta : ICar { public void Run() { Console.WriteLine("捷達車開動起來了..."); } } public class Crown : ICar { public void Run() { Console.WriteLine("皇冠車開動起來了..."); } } /// <summary> /// 用的抽象方法,考慮學員會有共性的內容 /// </summary> public abstract class BaseStudent { public string Name { get; set; } /// <summary> /// 給個建構函式,用來初始化名子 /// </summary> /// <param name="name"></param> protected BaseStudent(string name) { this.Name = name; } /// <summary> /// 學員要學習開車 /// 這裡用的是虛方法,實現可確定的基本操作 /// 由於每個學員學習過程可能不同,可進行重寫操作 /// </summary> /// <param name="car"></param> public virtual void LearnDrive(ICar car) { Console.WriteLine($"{this.Name}開始學車了"); car.Run(); } } public class Student : BaseStudent { public Student(string name) : base(name) { } /// <summary> /// 學員學習開車,只依賴了抽象(ICar介面) /// </summary> /// <param name="car"></param> public override void LearnDrive(ICar car) { //加入自己的內容 Console.WriteLine($"{this.Name}有些緊張,調整了下情緒"); base.LearnDrive(car); } }
在我們的場景中,程式碼如下所示
class Program { static void Main(string[] args) { try { //張三開皇冠車都是依賴上層抽象 //不同的學員開不同的車,就很容易處理了... BaseStudent student = new Student("張三"); ICar car = new Jetta(); student.LearnDrive(car); //張三開皇冠車 ICar crown = new Crown(); student.LearnDrive(crown); } catch (Exception e) { Console.WriteLine(e.Message); } Console.Read(); } }
執行結果,就不貼出來了。這裡注意到了沒有,是不是有些地方很熟悉,這不就是里氏替換原則嗎?其實,它們之間是相輔相成的,里氏替換是基礎,依賴倒置是方法和手段。剛開始瞭解設計模式時,我就被這兩個原則之間整的有點迷惑了,在程式碼設計的過程中,它們基本上都是同時出現的。