對於設計模式, 從本質上說, 其最大的用途就是適應需求的變化. 因為有了設計模式,我們可以在設計階段就為未來可能發生的變化留下足夠的空間. 我們通過一個建造現代化養豬場的故事, 來討論一下設計模式與需要變化之間的關係. (一)設計模式最根本的意圖是適應需求的變化 一個機器人研發小組研製了一種能自動喂 ...
對於設計模式, 從本質上說, 其最大的用途就是適應需求的變化. 因為有了設計模式,我們可以在設計階段就為未來可能發生的變化留下足夠的空間.
我們通過一個建造現代化養豬場的故事, 來討論一下設計模式與需要變化之間的關係.
(一)設計模式最根本的意圖是適應需求的變化
一個機器人研發小組研製了一種能自動喂豬的機器人, 於是有人投資興建了一座由機器人管理的養豬場. 這個養豬場要飼養的豬的品種包括:
- 大白豬: 又叫"大約克郡豬", 原產於英國. 全身白色,耳向前挺立,體長大,成年公豬體重三百到五百公斤,繁殖力強, 是全世界分佈最廣的豬型.
- 長白豬: 是"蘭德瑞斯豬" 在中國的統稱. 著名的腌肉型豬, 原產於丹麥. 全身白色, 體軀 特長, 耳大前垂, 背腰平直, 大腿豐滿. 皮薄瘦肉多.成年公豬體重四百到五百公斤, 要求較好的飼養管理條件.
- 波中豬: 是豬的著名品種, 原產於美國, 屬脂肪型, 已培育成肉用型. 全身黑色.成年公豬重三百九十到四百五十公斤, 早熟易肥, 但繁殖力較弱.
下麵我們來討論機器人小組的設計方案:
圖v1-1
用UML繪製的養豬場v1.0軟體動作機制如圖V1-1所示。最初的設計因為豬場只引進了大白豬品種,所以沒有考慮飼養其它種類豬的情況。
一個喂豬機器人管理了若幹頭大白豬。由於豬天性懶惰,機器人必須把飼料放到豬嘴邊,然後吆喝一句,“大白豬,吃!”,豬才會進食。
在v1.0版的程式動作下,養豬場運行狀況相當不錯,每頭豬都養得膘肥休壯,如果不是企業要不斷的追求剩餘價值,我們的故事也許就到此結束了。
隨著養豬場的蓬勃發展,養豬場老闆決定進軍國際市場。可是,不同地方的人喜歡吃不同品種的豬。為了適應這一需求的變化,養豬場新引進了一批長白豬。
問題出現了:喂豬機器人照例把飼料準備好,拿到每一頭豬面前,大叫一聲:”大白豬,吃!“,大白豬能像往常一樣愉快地進食。但輪到長白豬時,長白豬無動於衷。這下急壞了養豬場的老闆。為瞭解決問題,喂豬機器人的研發團隊緊急出動,當晚便改好了程式。
V2.0 程式如圖v2-1所示:
圖v2-1
經過此次修改,喂豬機器人在對待每一頭豬時,會先辯認出這頭豬的類型,如果這是一頭大白豬,它就會大叫一聲”大白豬,吃!“,如果是一頭長白豬,就叫”長白豬,吃“,經過這一個性,養豬場又恢復了平靜。
可剛過了幾天,類似的問題又再次出現,養豬場引進了幾頭波中豬!這下,機器人又不知道怎麼喂了。研發團隊又準備大動干戈、修改代碼了。老闆卻對研發團隊的做法表示不理解:“你們太不像話了,我是付了錢的,可每次我要擴大再生產的時候,你們都要耽誤我好幾天時間,重新修改什麼代碼,如果下次我要養雞、養青蛙了呢?”
這個現代化養豬場出現的問題其實就是需求變化的問題。設計模式可以使系統很容易地在某一特定的層面上適應需求的變化。使用設計模式後,系統就可以很好的滿足開閉原則:我們不用修改原來的代碼,而只需要添加新的代碼就可以滿足新的需求了。
從這個角度來看,使用設計模式的關鍵是預測未來可能發生的需求變化,併在設計過程中選用合適的設計模式。同時,我們也應該將設計模式應用於系統中那些明顯不穩定、極可能發生變化的部分,而不應該為了體驗創造的樂趣,把設計模式應用於那些永遠不會發生變化的組件中去。
例如,在養豬場系統中,當養豬場頭一回引進新品種長白豬時,我們就敏銳立即認識到豬的種類是一種易變的因素,這時就必須引入設計模式以適應這種需求的變化。
從這裡我們可總結出有關設計模式的第一個核心設計原則:
設計模式最根本的意圖是適應需求的變化,我們應只對變化或者可能變化的部分使用設計模式,對於不變的部分濫用設計模式就會造成“過渡設計”。
(二) 針對介面編程,而不要針對實現編程
現在,我們來看一下如何改進這個現代化養豬場的設計,使其能最大限度地適應需求變化。
首先,我們應該際加一個豬的抽象介面,該介面中定義了每一類豬共有的行為,而大白豬和長白豬則具體實現這些行為。系統中的大白豬和長白豬滿足完全替換原則,使用時客戶不用考慮豬的具體類型,就可以直接通過抽象的豬的介面來操作具體的大白豬和長白豬對象。
然後,我們修改喂豬機器人的代碼,使其不考慮豬的類型,只應用抽象的豬的介面來操作所有豬的對象實例。例如,喂豬時喂豬機器人需要吼叫的不再是"大白豬,吃!"或"長白豬,吃!"而是"豬,吃!"這種通過抽象類或抽象介面來操作對象的方式就是"針用介面編程"的方法,而此前那種通過具體的類來操作對象的方法可以被稱為"針對實現編程"的方法。
改進後的養豬場如圖v3-1所示。
圖v3-1
這個改進的養豬場會為我們帶來什麼好處呢?假設現在養豬場的老闆需要引進波中豬,他只要買來幾頭波中豬的仔豬,扔進養豬場就可以了。喂豬機器人的代碼不需要發生任何變化,它面對每一頭豬只要說"豬,吃!"所有類型的豬都可以愉快地進食。不管養豬場飼養的豬有多少種,喂豬機器人都會把豬喂得腰肥體壯。
添加了波中豬後的系統結構如圖v3-2所示。
圖v3-2
可以看到,喂豬機器人完全是針對介面進行編程的,當系統添加一個新的類型時,只需要添加新類型的代碼,而系統中原有的代碼不需要做任何的改變就可以適應新的需求一一這完全符合開閉原則。
V3版面向對象養豬廠的實現代碼如下:
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel; 4 using System.Data; 5 using System.Drawing; 6 using System.Linq; 7 using System.Text; 8 using System.Windows.Forms; 9 10 namespace PigFactoryV3 11 { 12 public partial class Form1 : Form 13 { 14 /* 15 * 豬悟能的博客 16 * http://www.cnblogs.com/hackpig/ 17 * 18 建成後的養豬場生意興隆, 老闆決定進軍國際市場, 但是為了迎合外國人口味, 引入了幾條長白豬. 19 因此,這裡豬的類型成了變化點. 20 我們把大白豬類, 改成豬介面, 這樣喂豬機器人的工作變成了, "豬,吃", 而不是原來的 "大白豬,吃" 21 即使老闆再增加幾頭波中豬, 我們也不再需要修改喂豬機器人的工作()了. 只需要增加一個波中豬的類實現豬介面 22 23 24 從這裡我們可以總結出有關設計模式的第一個核心設計原則: 25 26 設計模式最根本的意圖是適應需求變化, 我們應只對變化或者可能變化的部分使用設計模式,對於不變的部分濫用設計 27 模式就會造成"過度設計" 28 29 */ 30 31 32 public Form1() 33 { 34 InitializeComponent(); 35 36 feedPigRobot robot = new feedPigRobot("大潤發養豬場"); 37 robot.Attack(new dbPig(1)); 38 robot.Attack(new dbPig(2)); 39 robot.Attack(new dbPig(3)); 40 robot.Attack(new dbPig(4)); 41 robot.Attack(new dbPig(5)); 42 43 robot.Attack(new cbPig(6)); 44 robot.Attack(new cbPig(7)); 45 robot.Attack(new cbPig(8)); 46 robot.Attack(new cbPig(9)); 47 robot.Attack(new cbPig(10)); 48 49 rtbMsginfo.Text = robot.work(); 50 51 } 52 } 53 54 55 public abstract class feedPigFactory 56 { 57 private string _factoryName; 58 59 public string FactoryName 60 { 61 get { return _factoryName; } 62 set { _factoryName = value; } 63 } 64 65 } 66 67 public class feedPigRobot:feedPigFactory 68 { 69 IList<Ipig> pigList = new List<Ipig>(); 70 71 public feedPigRobot(string factoryName) 72 { 73 base.FactoryName = factoryName; 74 } 75 76 public void Attack(Ipig pig) 77 { 78 pigList.Add(pig); 79 } 80 81 public string work() 82 { 83 string msgstr = string.Empty; 84 foreach (Ipig m in pigList) 85 { 86 msgstr += m.eat()+Environment.NewLine; 87 } 88 89 return string.Format("{0}{1}{2}", 90 base.FactoryName + Environment.NewLine, 91 "喂豬機器人開始工作...." + Environment.NewLine + Environment.NewLine, 92 msgstr); 93 } 94 } 95 96 97 98 public interface Ipig 99 { 100 101 int PigIndex 102 { 103 get; 104 set; 105 } 106 107 string eat(); 108 109 } 110 111 112 public class cbPig : Ipig 113 { 114 private int _pigIndex; 115 116 public int PigIndex 117 { 118 get { return _pigIndex; } 119 set { _pigIndex = value; } 120 } 121 public cbPig(int pignum) 122 { 123 this._pigIndex = pignum; 124 } 125 126 public string eat() 127 { 128 return string.Format("{0}[{1}]開始吃.", "長白豬", _pigIndex); 129 } 130 } 131 132 133 134 public class dbPig :Ipig 135 { 136 private int _pigIndex; 137 138 public int PigIndex 139 { 140 get { return _pigIndex; } 141 set { _pigIndex = value; } 142 } 143 public dbPig(int pignum) 144 { 145 this._pigIndex = pignum; 146 } 147 148 public string eat() 149 { 150 return string.Format("{0}[{1}]開始吃.", "大白豬", _pigIndex); 151 } 152 } 153 154 }
運行結果如下:
代碼分析:
(1) feedPigRobot是喂豬機器人類
其成員函數 Attack 的參數是Ipig, 它是豬的介面
public void Attack(Ipig pig)
這個參數可以接納 dbPig , 大白豬類的實例, 或者是 cbPig, 長白豬類的實例.
work() 成員函數 遍歷所有IPig的"豬"對象, 調用它的eat()方法. (完成對所有豬喂食的操作)
foreach (Ipig m in pigList) { msgstr += m.eat()+Environment.NewLine; }
(2) 有了豬的抽象介面(Ipig), 它抽象出了豬共有了行為"吃", 即方法eat(). 喂豬機器人只對這個豬抽象介面喂食, 就不需要知道喂的究竟是長白豬,還是大白豬了.
養豬場的老闆還曾經提到過養雞和養青蛙。對此,我們必須明確該需求是否是合理的需求,系統是否需要適應這一需求變化。一般說來,養豬場是不會養雞、養青蛙的,我們沒必要為此多費心思。但如果老闆故意刁難的話,我們也不是沒有解決方案:適應這一需求變化的方法是提取出一個更一般的"動物介面"機器人完全使用"動物介面"類來操作所有的對象。
現在,面向對象的現代化養豬場又欣欣向榮地發展起來了。但我們不能放鬆警惕,變化的需求隨時都會出現。例如這一次,養豬場的老闆突然覺得,老從外面引進仔豬太虧,他希望能在養豬場內部建造一個繁殖基地,自產自銷。於是,我們建造了二個豬工廠,最初的豬工廠結構如圖V5-1所示。
圖v5-1
不難發現,在實現豬工廠時,我們又陷入了針對實現編程的陷阱。管理員使用豬工廠來選擇繁殖哪種類型的仔豬,豬工廠根據管理員的要求執行不同的繁殖過程,繁殖不同類型的仔豬。對於系統中己有的大白豬和長白豬,這沒有問題,但是當我們想繁殖波中豬時,問題又產生了,豬工廠的代碼必須修改。顯然,我們必須想辦法來隔離有關對象創建的代碼,以適應需求變化。設計模式中的創建型模式恰恰可以滿足我們的需要。
為了隔離具體的繁殖過程,我們可以定義一個豬工廠的抽象接U類,其派生類大自豬工廠和長白豬工廠具體地實現介面中的繁殖行為。這樣,和具體實現相關的代碼被推遲到了具體的派生類工廠中去實現,我們在系統外只要用不同的派生類工廠調用繁殖方法,就可以繁殖出不同的仔豬了。修改後的結構如圖V6-1所示。
這一改進為我們帶來的好處是,當我們要添加一種豬的類型時,也相應地添加繁殖這種豬的工廠,系統內原布的代碼不需要改變。這時,豬工廠負責繁殖仔豬,然後把仔豬交給喂豬機器人,這些仔豬的生、老、病、死就完全由喂豬機器人來負責了。為了在這一結構中添加波中豬,我們需要做的事情有添加波中豬類、添加波中豬工廠類、修改管理員繁殖仔豬的代碼等,這些工作都是在系統外完成的,與系統內原有的代碼無關(如圖V6-1所示)。
圖v6-1
面向對象養豬廠V6版實現代碼如下:
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel; 4 using System.Data; 5 using System.Drawing; 6 using System.Linq; 7 using System.Text; 8 using System.Windows.Forms; 9 10 namespace PigFactoryV6 11 { 12 public partial class Form1 : Form 13 { 14 /* 15 * 豬悟能的博客 16 * http://www.cnblogs.com/hackpig/ 17 * 18 突然有一天,老闆覺得從外面進豬仔太虧, 他希望在養豬場內部建立一個繁殖基地, 自產自銷. 19 於是, 軟體團隊建立了一個豬工廠. 20 21 現在, 養豬場完全可以由自產的仔豬開始喂養了. 22 無論是增加要喂食豬的品種, 還是繁殖新的豬的品種, 我們現在都不用改動原有的代碼. 23 只需要增加新的豬品種的類, 實現豬工廠介面, 和豬介面就可以了. 24 25 由些我們總結出設計模式第二個核心設計原則: 26 27 儘量針對介面編程, 而不要針對實現編程. 針對介面編程的組件不需要知道對象的具體 28 類型和實現, 只需要知道抽象類定義了哪些介面, 這減少了實現上的依賴關係. 29 30 */ 31 32 public Form1() 33 { 34 InitializeComponent(); 35 36 feedPigRobot robot1 = new feedPigRobot("大潤發養豬場"); 37 adminMan man1=new adminMan(2); 38 foreach (Ipig m in man1.Piglist) 39 robot1.Attack(m); 40 this.rtbMsgInfo.Text = robot1.work(); 41 42 } 43 } 44 45 46 public class adminMan 47 { 48 private IList<Ipig> _piglist; 49 50 public IList<Ipig> Piglist 51 { 52 get { return _piglist; } 53 set { _piglist = value; } 54 } 55 56 public adminMan(int breedType) 57 { 58 IList<Ipig> piglist1 = new List<Ipig>(); 59 switch (breedType) 60 { 61 case 1: //大白豬 62 foreach (Ipig m in new breedDBpig().breedPig()) 63 piglist1.Add(m); 64 break; 65 case 2: //長白豬 66 foreach (Ipig m in new breedCBpig().breedPig()) 67 piglist1.Add(m); 68 break; 69 default: 70 break; 71 } 72 if (piglist1.Count > 0) 73 _piglist = piglist1; 74 75 } 76 } 77 78 79 80 public interface IbreedPig 81 { 82 IList<Ipig> breedPig(); 83 } 84 85 public class breedDBpig : IbreedPig 86 { 87 private IList<Ipig> pigList = new List<Ipig>(); 88 public IList<Ipig> breedPig() 89 { 90 Random ran = new Random(DateTime.Now.Millisecond); 91 for (int i = 0; i < ran.Next(2, 20); i++) 92 pigList.Add(new dbPig(i)); 93 return pigList; 94 } 95 } 96 97 public class breedCBpig : IbreedPig 98 { 99 private IList<Ipig> pigList = new List<Ipig>(); 100 public IList<Ipig> breedPig() 101 { 102 Random ran = new Random(DateTime.Now.Millisecond); 103 for (int i = 0; i < ran.Next(2, 20); i++) 104 pigList.Add(new cbPig(i)); 105 return pigList; 106 } 107 } 108 109 110 public abstract class feedPigFactory 111 { 112 private string _factoryName; 113 114 public string FactoryName 115 { 116 get { return _factoryName; } 117 set { _factoryName = value; } 118 } 119 120 } 121 122 public class feedPigRobot : feedPigFactory 123 { 124 IList<Ipig> pigList = new List<Ipig>(); 125 126 public feedPigRobot(string factoryName) 127 { 128 base.FactoryName = factoryName; 129 } 130 131 public void Attack(Ipig pig) 132 { 133 pigList.Add(pig); 134 } 135 136 public string work() 137 { 138 string msgstr = string.Empty; 139 foreach (Ipig m in pigList) 140 { 141 msgstr += m.eat() + Environment.NewLine; 142 } 143 144 return string.Format("{0}{1}{2}", 145 base.FactoryName + Environment.NewLine, 146 "喂豬機器人開始工作...." + Environment.NewLine + Environment.NewLine, 147 msgstr); 148 } 149 } 150 151 152 153 public interface Ipig 154 { 155 156 int PigIndex 157 { 158 get; 159 set; 160 } 161 162 string eat(); 163 164 } 165 166 167 public class cbPig : Ipig 168 { 169 private int _pigIndex; 170 171 public int PigIndex 172 { 173 get { return _pigIndex; } 174 set { _pigIndex = value; } 175 } 176 public cbPig(int pignum) 177 { 178 this._pigIndex = pignum; 179 } 180 181 public string eat() 182 { 183 return string.Format("{0}[{1}]開始吃.", "長白豬", _pigIndex); 184 } 185 } 186 187 188 189 public class dbPig : Ipig 190 { 191 private int _pigIndex; 192 193 public int PigIndex 194 { 195 get { return _pigIndex; } 196 set { _pigIndex = value; } 197 } 198 public dbPig(int pignum) 199 { 200 this._pigIndex = pignum; 201 } 202 203 public string eat() 204 { 205 return string.Format("{0}[{1}]開始吃.", "大白豬", _pigIndex); 206 } 207 } 208 209 210 }
運行效果:
代碼說明:
(1) 下麵代碼可以看到,
feedPigRobot是喂豬機器人類, adminMan是繁殖管理類, 它使用工廠方式創建對應品種的豬.
feedPigRobot robot1 = new feedPigRobot("大潤發養豬場"); adminMan man1=new adminMan(2); foreach (Ipig m in man1.Piglist) robot1.Attack(m); this.rtbMsgInfo.Text = robot1.work();
adminMan()有個構造函數參數, 傳入參數1,或者2, 表示使用工廠方式創建隨機數量的大白豬或者是長白豬.
IList<Ipig> piglist1 = new List<Ipig>(); switch (breedType) { case 1: //大白豬 foreach (Ipig m in new breedDBpig().breedPig()) piglist1.Add(m); break; case 2: //長白豬 foreach (Ipig m in new breedCBpig().breedPig()) piglist1.Add(m); break; default: break; }
(2) 本常式傳入adminMan()參數為2, 因此只是隨機創建了一批長白豬
由此,我們可以總結出設計模式的第二個核心設計原則:
儘量針對介面編程,而不要針對實現編程。針對介面編程的組件不需要知道對象的具體類型和實現,只需要知道抽象類定義了哪些介面,這減少了實現上的依賴關係。
未完待續......