第九章 設計模式與原則
軟體設計模式(Design pattern)是一套被反覆使用的程式碼設計經驗總結。使用設計模式是為了可重用程式碼、讓程式碼更容易被他人理解、保證程式碼可靠性。好的設計,成就好的作品。但在軟體設計的過程中,若有一些設計原則(Design Principle)的約束,那我們的軟體會重構得更好。設計模式和設計原則博大精深,需要我們長時間的實踐和總結才能真正領悟到其真諦,本章首先以“觀察者模式”為例,介紹設計模式在Windows Forms中的應用(其他常用設計模式略),之後詳細介紹五大設計原則(簡稱Solid原則)。
9.1軟體的設計模式
9.1.1觀察者模式
程式的執行意味著模組與模組之間、物件與物件之間不停地有資料交換,觀察者模式強調的就是,當一個目標本身的狀態發生改變時(或者滿足某一條件),它會主動發出通知,通知對該變化感興趣的其他物件,如果將通知者稱為“Subject”(主體),將被通知者稱為“Observer”(觀察者),具體結構圖如下:
圖9-1 觀察者模式中類關係圖
如上圖9-1所示,圖中將主體和觀察者的邏輯抽象出來兩個介面,分別為:ISubject和IObserver,ISubject介面中包含一個通知觀察者的NotifyObservers方法、一個新增觀察者的AddObserver方法和一個RemoveObserver方法,IObserver介面中則只包含一個接受通知的Notify方法,ISubject和IObserver介面的關係為:一對多,一個主體可以通知多個觀察者。
具體程式碼實現如下:
1 //Code 9-1 2 3 interface ISubject //NO.1 4 5 { 6 7 void NotifyObservers(string msg); 8 9 void AddObserver(IObserver observer); 10 11 void RemoveObserver(IObserver observer); 12 13 } 14 15 interface IObserver //NO.2 16 17 { 18 19 void Notify(string msg); 20 21 } 22 23 class MySubject:ISubject 24 25 { 26 27 //… 28 29 ArrayList _observers_list = new ArrayList(); 30 31 public void AddObserver(IObserver observer) //NO.3 32 33 { 34 35 if(!_observers_list.Contains(observer)) 36 37 { 38 39 _observers_list.Add(observer); 40 41 } 42 43 } 44 45 public void RemoveObserver(IObserver observer) //NO.4 46 47 { 48 49 if(_observers_list.Contains(observer)) 50 51 { 52 53 _observers_list.Remove(observer); 54 55 } 56 57 } 58 59 public void NotifyObservers(string msg) //NO.5 60 61 { 62 63 foreach(IObserver observer in _observers_list) 64 65 { 66 67 observer.Notify(msg); 68 69 } 70 71 } 72 73 public void DoSomething() 74 75 { 76 77 //… 78 79 if(…) //NO.6 80 81 { 82 83 NotifyObservers(…); 84 85 } 86 87 } 88 89 } 90 91 class MyObserver:IObserver 92 93 { 94 95 public void Notify(string msg) 96 97 { 98 99 Console.WriteLine(“receive msg :” + msg); //NO.7 100 101 } 102 103 } 104 105 class YourObserver:IObserver 106 107 { 108 109 public void Notify(string msg) 110 111 { 112 113 //send email to others NO.8 114 115 } 116 117 }
如上程式碼Code 9-1中所示,NO.1和NO.2處分別定義了ISubject和IObserver介面,接著定義了一個具體的主體類MySubject,該類實現了ISubject介面,在AddObserver、RemoveObserver分別將觀察者加入或者移除集合_observers_list(NO.3和NO.4處),最後在NotifyObservers方法中,遍歷_observers_list集合,將通知傳送到每個觀察者(NO.5處),注意我們可以在DoSomething方法中當滿足某一條件時,通知觀察者(NO.6處)。我們使用IObserver介面定義了兩個具體的觀察者MyObserver和YourObserver,在兩者的Notify方法中分別按照自己的邏輯去處理通知資訊(一個直接將msg列印出來,一個將msg以郵件形式傳送給別人)(NO.7和NO.8處)。
現在我們可以將MySubject類物件當作一個具體的主體,將MyObserver類物件和YourObserver類物件當做具體的觀察者,那麼程式碼中可以這樣去使用:
1 //Code 9-2 2 3 ISubject subject = new MySubject(); 4 5 subject.AddObserver(new MyObserver()); //NO.1 6 7 subject.AddObserver(new YourObserver()); //NO.2 8 9 10 11 subject.NotifyObservers(“it's a test!”); //NO.3 12 13 (subject as MySubject).DoSomething(); //NO.4
如上程式碼Code 9-2所示,我們向主體subject中新增兩個觀察者(NO.1和NO.2處),之後使用ISubject.NotifyObservers方法通知觀察者(NO.3),另外,我們還可以使用MySubject.DoSomething方法去通知觀察者(當某一條件滿足時),兩個觀察者分別會做不同的處理,一個直接將“it's a test”字串列印輸出,而另一個則將字串以郵件的形式傳送給別人。
注:Code 9-2中,我們不能使用ISubject介面去呼叫DoSomething方法,而必須先將ISubject型別轉換成MySubject型別,因為DoSomething不屬於ISubject介面。
觀察者模式中,整個流程見下圖9-2:
圖9-2 觀察者模式中的執行流程
如上圖9-2所示,在有些情況中,NO.2處會做一些篩選,換句話說,主體有可能根據條件通知部分觀察者,NO.4處虛線框表示可選,如果主體關心觀察者的處理結果,那麼觀察者就應該將自己的處理結果返回給主體。“觀察者模式”是所有框架使用得最頻繁的設計模式之一,原因很簡單,“觀察者模式”分隔開了框架程式碼和框架使用者編寫的程式碼,它是“好萊塢原則”(Hollywood Principle,don't call us,we will call you)的具體實現手段,而“好萊塢原則”是所有框架都嚴格遵守的。
Windows Forms框架中的“觀察者模式”主要不是通過“介面-具體”這種方式去實現的,更多的是使用.NET中的“委託-事件”去實現,詳見下一小節。
9.1.2Windows Forms中的觀察者模式
在Windows Forms框架中,可以說“觀察者模式”無處不在,在第四章講Winform程式結構時已經有所說明,比如控制元件處理Windows訊息時,最終是以“事件”的形式去通知事件註冊者的,那麼這裡的事件註冊者就是觀察者模式中的“觀察者”,控制元件就是觀察者模式中的“主體”。我們回憶一下第四章中有關System.Windows.Forms.Control類的程式碼(部分):
1 //Code 9-3 2 3 class Control:Component 4 5 { 6 7 //… 8 9 public event EventHandler Event1; 10 11 public event EventHandler Event2; 12 13 protected virtual void WndProc(ref Message m) 14 15 { 16 17 switch(m.Msg) 18 19 { 20 21 case 1: //NO.1 22 23 { 24 25 //… 26 27 OnEvent1(…); 28 29 break; 30 31 } 32 33 case 2: //NO.2 34 35 { 36 37 OnEvent2(…); 38 39 break; 40 41 } 42 43 //… 44 45 } 46 47 } 48 49 protected virtual void OnEvent1(EventArgs e) 50 51 { 52 53 if(Event1 != null) 54 55 { 56 57 Event1(this,e); //NO.3 58 59 } 60 61 } 62 63 protected virtual void OnEvent2(EventArgs e) 64 65 { 66 67 if(Event2 != null) 68 69 { 70 71 Event2(this,e); //NO.4 72 73 } 74 75 } 76 }
如上程式碼Code 9-3所示,在Control類的WndProc視窗過程中的switch/case塊中,會根據不同的Windows訊息去激發不同的事件(NO.1和NO.2處),由於WndProc是一個虛方法,所有在任何一個Control的派生類中,均可以重寫WndProc虛方法,處理Windows訊息,然後以“事件”的形式去通知事件註冊者。
如果我們在Form1中註冊了一個Button類物件btn1的Click事件,那麼btn1就是觀察者模式中的“主體”,Form1(的例項)就是觀察者模式中的“觀察者”,如下程式碼:
1 //Code 9-4 2 3 class Form1:Form 4 5 { 6 7 //… 8 9 public Form1() 10 11 { 12 13 InitializeComponent(); 14 15 btn1.Click += new EventHandler(btn1_Click); //NO.1 16 17 } 18 19 private void btn1_Click(object sender,EventArgs e) //NO.2 20 21 { 22 23 //… 24 25 } 26 27 }
如上圖Code 9-4程式碼所示,我們在Form1的構造方法中註冊了btn1的Click事件(NO.1處),那麼btn1就是“主體”,Form1(的例項)就是“觀察者”,當btn1需要處理Windows訊息時,就會激發事件,通知Form1(的例項)。
Windows Forms框架正是使用“觀察者模式”實現了框架程式碼與框架使用者編寫的程式碼相分離。
注:我們可以認為,事件的釋出者等於觀察者模式中的“主體”(Subject),而事件的註冊者等於觀察者模式中的“觀察者”,有關“事件程式設計”,請參考第六章。
9.2軟體的設計原則
9.2.1Solid原則介紹
“Solid原則”代表軟體設計過程中常見的五大原則,分別為:
(1)S:單一職責原則(Single Responsibility Principle):
一個類應該只負責一個(種)事情;
(2)O:開閉原則(Open Closed Principle):
優先選擇在已有的型別基礎上擴充套件新的型別,避免修改已有型別(已有程式碼);
(3)L:里氏替換原則(Liskov Substitution Principle):
任何基類出現的地方,派生類一定可以代替基類出現,言下之意就是,派生類一定要具備基類的所有特性;
(4)I:介面隔離原則(Interface Segregation Principle):
一個型別不應該去實現它不需要的介面,換句話說,介面應該只包含同一類方法或屬性等;
(5)D:依賴倒置原則(Dependency Inversion Principle):
高層模組不應該依賴於低層模組,高層模組和低層模組應該同時依賴於一個抽象層(介面層)。
設計模式相對來講更具體,每種設計模式幾乎都能解決現實生活中某一具體問題,而設計原則相對來講更抽象,它是我們在軟體設計過程中的行為準則,並不能用在某一具體情景之中。以上五大原則單從字面上理解起來不太直觀,下面依次舉例說明之。
9.2.2單一職責原則(SRP)
“一個類應該只負責一個(種)事情”,原因很簡單,負責的事情越多,那麼這個型別出錯或者需要修改的概率越大,假如現在有一個超市購物的會員類VIP:
1 //Code 9-5 2 3 class VIP:IData 4 5 { 6 7 public void Read() 8 9 { 10 11 try 12 13 { 14 15 //read db here… 16 17 } 18 19 catch(Exception ex) 20 21 { 22 23 System.IO.File.WriteAllText(@"c:\errorlog.txt", ex.ToString()); //NO.1 24 25 } 26 27 } 28 29 } 30 31 interface IData //NO.2 32 33 { 34 35 void Read(); 36 37 }
如上程式碼Code 9-5所示,定義了一個訪問資料庫的IData介面(NO.2處),該介面包含一個Read方法,用來讀取會員資訊,會員類VIP實現了IData介面,在編寫Read方法時,我們捕獲訪問資料庫的異常後,直接將錯誤資訊寫入到了日誌檔案(NO.1處)。這段程式碼看似沒有任何問題,但是後期確會暴露出設計不合理的現象,如果我們現在不想把日誌檔案輸出到本地C盤(NO.1處),而是輸出到D盤,那我們需要修改VIP的原始碼,沒錯,本來我們只是想修改日誌部分的邏輯,現在卻不得不更改VIP類的程式碼。出現這種現象的原因就是VIP類幹了本不應該它乾的事情:記錄日誌。就像下面這張圖描述的:
圖9-3 一個負責了太多事情的工具
如上圖9-3所示,一把包含太多功能的刀,如果哪天某個功能壞掉,我們不得不將整把刀送去維修。正確解決以上問題的做法就是將日誌邏輯與VIP類分開,程式碼如下:
1 //Code 9-6 2 3 class Logger //NO.1 4 5 { 6 7 public void WriteLog(string error) 8 9 { 10 11 System.IO.File.WriteAllText(@"c:\errorlog.txt", error); 12 13 } 14 15 } 16 17 class VIP:IData 18 19 { 20 21 private Logger _logger = new Logger(); //NO.2 22 23 public void Read() 24 25 { 26 27 try 28 29 { 30 31 //read db here… 32 33 } 34 35 catch (Exception ex) 36 37 { 38 39 _logger.WriteLog(ex.ToString()); //NO.3 40 41 } 42 43 } 44 }
如上程式碼Code 9-6所示,我們定義了一個型別Logger專門負責記錄日誌(NO.1處),在VIP類中通過Logger型別來記錄錯誤資訊(NO.2和NO.3處),這樣一來,當我們需要修改日誌部分的邏輯時,不需要再動VIP類的程式碼。
單一職責原則提倡我們將複雜的功能拆分開來,分配到每個單獨的型別當中,至於什麼是複雜的功能,到底將功能拆分到什麼程度,這個是沒有標準的,如果記錄日誌是一個繁瑣的過程(本小節示例程式碼相對簡單),你還可以將日誌類Logger的功能再繼續拆分。
9.2.3開閉原則(OCP)
“優先選擇在已有的型別基礎上擴充套件新的型別,避免修改已有型別(已有程式碼)”,修改已有程式碼就意味著需要重新測試原有的功能,因為任何一次修改都可能影響已有功能。如果在普通VIP顧客的基礎之上,多了白銀會員(silver vip)顧客,這兩種顧客在購物時的折扣不一樣,如果VIP類定義如下(不全):
1 //Code 9-7 2 3 class VIP:IData 4 5 { 6 7 private int _viptype; //vip type NO.1 8 9 //… 10 11 public virtual void Read() 12 13 { 14 15 //… 16 17 } 18 19 public double GetDiscount(double totalSales) 20 21 { 22 23 if(_viptype == 1) //vip 24 25 { 26 27 return totalSales – 10; //NO.2 28 29 } 30 31 else //silver vip 32 33 { 34 35 return totalSales – 50; //NO.3 36 37 } 38 39 } 40 41 }
如上程式碼Code 9-7所示,我們在定義VIP類的時候,使用_viptype欄位來區分當前顧客是普通VIP還是白銀VIP(NO.1處),在打折方法GetDiscount中,根據不同的VIP種類返回不同打折後的價格(NO.2和NO.3處),這段程式碼的確也可以執行的很好,但是後期還是會暴露出設計不合理的地方,如果現在不止增加一個白銀會員,還增加了一個黃金會員(gold vip),那麼我們不得不再去修改GetDiscount方法中的if/else塊,修改意味著原有功能可能會出現bug,因此我們不得不再去測試之前所有使用到了VIP這個型別程式碼。出現這個問題的主要原因就是我們從一開始設計VIP類的時候就不合理:沒有考慮到將來可能會有普通會員的衍生體出現。
如果我們一開始在設計VIP類的時候就應用了物件導向思想,我們的VIP類可以這樣定義:
1 //Code 9-8 2 3 interface IDiscount //NO.1 4 5 { 6 7 double GetDiscount(double totalSales); 8 9 } 10 11 class VIP:IData,IDiscount 12 13 { 14 15 //… 16 17 public virtual void Read() 18 19 { 20 21 //… 22 23 } 24 25 public virtual double GetDiscount(double totalSales) //NO.2 26 27 { 28 29 return totalSales – 10; 30 31 } 32 33 } 34 35 class SilverVIP:VIP 36 37 { 38 39 //… 40 41 public override double GetDiscount(double totalSales) 42 43 { 44 45 return totalSales – 50; //NO.3 46 47 } 48 49 } 50 51 class GoldVIP:SilverVIP 52 53 { 54 55 //… 56 57 public override double GetDiscount(double totalSales) 58 59 { 60 61 return totalSales – 100; //NO.4 62 63 } 64 65 }
如上程式碼Code 9-8所示,我們定義了一個IDiscount的介面(NO.1處),包含一個打折的GetDiscount方法,接下來讓VIP類實現了IDiscount介面,將介面中的GetDiscount方法定義為虛方法(NO.2處),後面的白銀會員(SilverVIP)繼承自VIP類、黃金會員(GoldVIP)繼承自SilverVIP類,並分別重寫GetDiscount虛方法,返回相應的打折之後的總價格(NO.3和NO.4處)。這樣一來,新增加會員型別不需要去修改VIP類,也不影響之前使用了VIP類的程式碼。
下圖9-4顯示了重新設計VIP類的前後區別:
圖9-4 繼承發生之後
如上圖9-4所示,圖中左邊部分表示不採用繼承的方式去實現普通VIP、白銀VIP和黃金VIP的打折邏輯,可以看出,每次需要增加一種會員時,都必須去修改VIP類的程式碼,圖中右邊部分表示採用繼承方式之後,每種會員均定義成一個型別,每個型別均可以負責自己的打折邏輯,以後不管新增多少種會員,均可以定義新的派生類,在派生類中定義新的打折邏輯。
注:派生類中只需要重寫打折的邏輯,不需要重新去定義讀取資料庫的邏輯,因為這個邏輯在基類和派生類中並沒有發生變化。
9.2.4里氏替換原則(LSP)
“任何基類出現的地方,派生類一定可以代替基類出現,言下之意就是,派生類一定要具備基類的所有特性”,意思就是說,如果B是A的兒子,那麼B一定可以代替A去做任何事情,否則,B就不應該是A的兒子。我們在設計型別的時候,往往不去注意一個型別是否真的應該去繼承另外一個型別,很多時候我們只是為了遵從所謂的“OO”思想。如果現在有一個管理員類Manager,因為管理員也需要讀取資料庫,所以我們讓它繼承自VIP類,程式碼如下:
1 //Code 9-9 2 3 class Manager:VIP 4 5 { 6 7 //… 8 9 public override void Read() 10 11 { 12 13 //… 14 15 } 16 17 public override double GetDiscount(double totalSales) 18 19 { 20 21 throw new Exception(“don't have this function!”); //NO.1 22 23 } 24 25 }
如上程式碼Code 9-9所示,我們定義Manager類,讓其繼承自VIP類,由於Manager類並沒有“打折扣”的邏輯,因此我們重寫GetDiscount方法時,丟擲“don't have this function!”這樣的異常(NO.1處),接下來我們可能編寫出如下這樣的程式碼:
1 //Code 9-10 2 3 List<VIP> vips = new List<VIP>(); //NO.1 4 5 vips.Add(new VIP()); 6 7 vips.Add(new SilverVIP()); 8 9 vips.Add(new GoldVIP()); 10 11 vips.Add(new Manager()); 12 13 //… 14 15 foreach(VIP v in vips) 16 17 { 18 19 /… 20 21 double d = v.GetDiscount(…); //NO.2 22 23 //… 24 25 }
如上程式碼Code 9-10所示,我們定義了一個VIP型別的容器(NO.1處),依次將VIP、SilverVIP、GoldVIP以及Manager型別物件加入容器,最後通過foreach遍歷該容器,呼叫容器中每個元素的GetDiscount方法(NO.2處),此段程式碼一切正常通過編譯,因為編譯器承認“基類出現的地方,派生類一定能夠代替其出現”,但事實上,程式執行之後,在呼叫Manager類物件的GetDiscount虛方法時會丟擲異常,造成這個現象的主要原因就是,我們根本沒搞清楚類的繼承關係,Manager類雖然也要訪問資料庫,但是它並非屬於VIP的一種,也就是說,Manager類不應該是VIP類的兒子,如下圖9-5:
圖9-5Manager錯誤的繼承關係
如上圖9-5所示,Manager類雖然需要讀取資料庫,但是它並不需要有與“折扣”相關的操作,而且它根本不屬於一種VIP的衍生物,正確的做法是讓Manager類直接實現IData介面即可,如下程式碼:
1 //Code 9-11 2 3 class Manager:IData 4 5 { 6 7 //… 8 9 public void Read() 10 11 { 12 13 //… 14 15 } 16 17 }
如上程式碼Code 9-11所示,Manager實現了IData介面之後,不再跟VIP類有關聯,這樣一來,前面Code 9-10程式碼在編譯時,就會通不過,
1 //Code 9-12 2 3 List<VIP> vips = new List<VIP>(); //NO.1 4 5 vips.Add(new VIP()); 6 7 vips.Add(new SilverVIP()); 8 9 vips.Add(new GoldVIP()); 10 11 vips.Add(new Manager()); //NO.2
如上程式碼Code 9-12所示,編譯器會在NO.2處報錯,原因很簡單,Manager既然不是VIP的派生類了,就不能代替VIP出現。
如果兩個類從邏輯上就沒有衍生的關係,就不應該有相互繼承出現,見下圖9-6:
圖9-6 沒有衍生關係的兩個物體
如上圖9-6所示,狗跟貓兩種動物沒有衍生關係,狗類(Dog)不能繼承自貓類(Cat),貓類也不能繼承自狗類,但是他們都可以同時繼承自動物類(Animal)。
9.2.5介面隔離原則(ISP)
“一個型別不應該去實現它不需要的介面,換句話說,介面應該只包含同一類方法或屬性等”,如果把所有的方法都放在一個介面中,那麼實現了該介面的型別必須實現介面中的全部方法(即使不需要),同理,在一個已經很穩定的系統中,不應該再去修改已經存在的介面,因為這會影響到之前所有實現該介面的型別。現在如果需要新增加一種VIP顧客(SuperVIP),允許它修改資料庫,我們可能這樣去修改IData介面:
1 //Code 9-13 2 3 interface IData 4 5 { 6 7 void Read(); 8 9 void Write(); //NO.1 10 11 }
如上程式碼Code 9-13所示,我們修改已經存在的IData介面,使其包含一個寫資料庫的Write方法(NO.1處),滿足SuperVIP類的需要,這個方法看似可以,但是它要求我們修改其他已經實現了IData介面的型別,比如前面的VIP類,只要涉及到VIP類的更改,那麼其他所有使用到了VIP類的地方都得重新測試,可以看出,這會影響整個已經存在的系統。正確的做法應該是,新增加一個介面IData2,將資料庫的寫入方法放在該介面中,讓SuperVIP類實現該介面,程式碼如下:
1 Code 9-14 2 3 interface IData2:IData //NO.1 4 5 { 6 7 void Write(); 8 9 } 10 11 class SuperVIP:IData2,IData 12 13 { 14 15 public void Read() 16 17 { 18 19 //… 20 21 } 22 23 public void Write() 24 25 { 26 27 //… 28 29 } 30 31 }
如上程式碼Code 9-14所示,我們定義了一個新的介面IData2(NO.1處),該介面包含一個Write方法,讓SuperVIP類實現該介面,這樣一來,整個過程不會影響已經存在的VIP類。
9.2.6依賴倒置原則(DIP)
“高層模組不應該依賴於低層模組,高層模組和低層模組應該同時依賴於一個抽象層(介面層)”,本原則目的很明確,就是為了降低模組之間的耦合度,我們觀察一下9.2.2小節示例程式碼中的VIP類和Logger類,很明顯,VIP類直接依賴於Logger類,如果我們想換種方式記錄日誌的話(也就是改變記錄日誌的邏輯),必須得重新修改Logger類中的程式碼,現在如果讓VIP類依賴於一個抽象介面ILog,其他所有記錄日誌的型別同時也依賴於ILog介面,那麼整個系統就會更加靈活,
1 Code 9-15 2 3 interface ILog //NO.1 4 5 { 6 7 void Log(string error); 8 9 } 10 11 class FileLogger:ILog //NO.2 12 13 { 14 15 public void Log(string error) 16 17 { 18 19 //write error log to local file 20 21 } 22 23 } 24 25 class EmailLogger:ILog //NO.3 26 27 { 28 29 public void Log(string error) 30 31 { 32 33 //send error log as email 34 35 } 36 37 } 38 39 class NotifyLogger:ILog //NO.4 40 41 { 42 43 public void Log(string error) 44 45 { 46 47 //notify other modules 48 49 } 50 51 } 52 53 class VIP:IData,IDiscount //NO.5 54 55 { 56 57 //… 58 59 ILog _logger; 60 61 public VIP(ILog logger) //NO.6 62 63 { 64 65 _logger = logger; 66 67 } 68 69 public virtual void Read() 70 71 { 72 73 try 74 75 { 76 77 //…read db here 78 79 } 80 81 catch(Exception ex) 82 83 { 84 85 _logger.Log(ex.ToString()); //NO.7 86 87 } 88 89 } 90 91 public virtual double GetDiscount(double totalSales) 92 93 { 94 95 return totalSales – 10; 96 97 } 98 99 }
如上程式碼Code 9-15所示,我們定義了一個日誌介面ILog作為抽象層(NO.1處),之後定義了各種各樣的低層日誌模組(NO.2、NO.3和NO.4處),這些記錄日誌的類均依賴(實現)ILog這個抽象介面,之後我們在定義VIP類時,不再讓它具體依賴於某個日誌類,換句話說,不再讓高層模組直接依賴低層模組,取而代之的是,讓VIP類依賴於ILog這個抽象介面(NO.6處),我們在使用VIP類的時候,可以根據需要給它傳遞不同的日誌類物件(也可以是除了示例程式碼中的三個以外自定義型別,只要實現了ILog介面),程式執行後,會將錯誤日誌記錄到相應位置(NO.7處)。我們可以這樣使用VIP類:
1 Code 9-16 2 3 IData v = new VIP(new FileLogger()); 4 5 v.Read(); //NO.1 6 7 8 9 IData v2 = new VIP(new EMailLogger()); 10 11 v2.Read(); //NO.2 12 13 14 15 IData v3 = new VIP(new NotifyLogger()); 16 17 v3.Read(); //NO.3
如上程式碼Code 9-16所示,NO.1處如果出現異常,錯誤日誌會儲存到檔案,NO.2處如果出現異常,錯誤日誌將會通過郵件傳送給別人,NO.3處如果出現異常,VIP物件會自動把錯誤資訊通知給別的模組。
依賴倒置原則提倡模組與模組之間不應該有直接的依賴關係,見下圖9-7:
圖9-7 依賴倒置發生前後
如上圖9-7所示,圖中左邊部分表示依賴倒置之前高層模組與低層模組之間的依賴關係,圖中右邊部分表示依賴倒置發生之後,高層模組與低層模組之間的依賴關係,很明顯,依賴倒置發生後,高層模組不再直接受低層模組控制,高層模組與低層模組沒有具體的對應關係,靈活性增加,耦合度降低。
圖9-8 接力賽跑中的接力棒
如上圖9-8所示,接力過程中前後兩人沒有具體對應關係。
注:依賴倒置原則是每個框架都必須遵循的,框架不可能受框架的使用者控制,換句話說,框架作為“高層模組”,不應該依賴於框架使用者編寫的程式碼(低層模組),而應該均依賴於一個抽象層,所以我們在使用框架編寫程式碼時,大部分時候均以框架庫作為基礎,從已有的型別或者介面(抽象層)派生出新的型別。
9.3設計模式與設計原則對框架的意義
“IT語境中的框架,特指為解決一個開放性問題而設計的具有一定約束性的支撐結構。在此結構上可以根據具體問題擴充套件、安插更多的組成部分,從而更迅速和方便地構建完整的解決問題的方案。”——摘自網際網路
上面是一段摘自網際網路上描述“框架”的話,從這段話中我們瞭解到,首先,每個框架解決問題的範圍是有限的,比如Windows Forms框架只會幫助我們完成Windows桌面應用程式的開發,這就是它的“約束性”,其次,框架本身解決不了什麼特定的問題,它只給瞭解決特定問題的相關模組(或者元件)一個可插接、可組合的底子,這個底子為我們解決實際具體問題提供了支援,這就是框架的“支撐性”,見下圖9-9:
圖9-9 框架使用前後
如上圖9-9所示,圖中左邊部分表示使用框架之前,整個系統均由開發者編寫程式碼的結構圖,我們可以看見,無論系統的“系統執行邏輯”還是“業務處理邏輯”均由開發者負責,開發者自己呼叫自己的程式碼,整個系統的執行流程由開發者控制;圖中右邊部分表示使用了框架之後,“系統執行邏輯”由框架接管了,開發者只需要把精力集中在“業務邏輯處理”之上(Windows Forms框架接管了訊息迴圈、訊息處理等,負責了整個Winform程式的運轉),除此之外,還有一個非常大而且非常重要的改變:開發者不再(幾乎不)自己呼叫自己的程式碼了,自己編寫的程式碼均由框架呼叫,系統執行的控制權交給了框架。這就是所有框架所必須滿足的“好萊塢原則”(Hollywood Principle,don't call us,we will call you),“好萊塢原則”跟“控制轉換原則”(IoC,Inversion of Control)類似,參見前面章節,可以瞭解框架是怎樣反過來控制程式的執行。
我們在使用框架開發應用程式去解決實際具體的問題時,框架避免不了會與我們開發者編寫的程式碼進行互動,這就會產生一個問題,那就是怎樣去把握框架程式碼和框架使用者編寫程式碼兩者之間的關聯性,也就是我們常說的“高內聚,低耦合”。“高內聚,低耦合”在框架中要求更高,因為框架的使用人群和範圍比一般普通系統更大更廣泛,優秀的框架要想使用壽命更長口碑更好,就要求框架能在使用後期能夠更容易升級、更方便擴充套件新的功能來滿足使用者的各種需要,而這些大部分取決於框架最開始的設計好壞,正確地使用各種“設計模式”以及嚴格地遵守各種“設計原則”是決定框架後期能否應付各種變更、升級擴充套件的重要因素。