有了翅膀才能飛, 欠缺靈活的代碼就象凍壞了翅膀的鳥兒。不能飛翔,就少了幾許靈動的氣韻。咱們須要給代碼帶去溫暖的陽光, 讓僵冷的翅膀從新飛起來。程序員
結合實例, 經過應用OOP、設計模式和重構,你會看到代碼是怎樣一步一步復活的。設計模式
爲了更好的理解設計思想, 實例儘量簡單化。 但隨着需求的增長,程序將愈來愈複雜。app
此時就有修改設計的必要, 重構和設計模式就能夠派上用場了。 最後當設計漸趨完美后,你會發現, 即便需求不斷增長,你也能夠神清氣閒,不用爲代碼設計而煩惱了。ide
假定咱們要設計一個媒體播放器。 該媒體播放器目 前只支持音頻文件 mp3 和 wav。學習
若是不談設計, 設計出來的播放器可能很簡單:spa
1 public class MediaPlayer 2 { 3 private void PlayMp3() 4 { 5 MessageBox.Show("Play the mp3 file."); 6 } 7 private void PlayWav() 8 { 9 MessageBox.Show("Play the wav file."); 10 } 11 public void Play(string audioType) 12 { 13 switch (audioType.ToLower()) 14 { 15 case ("mp3"): 16 PlayMp3(); 17 break; 18 case ("wav"): 19 PlayWav(); 20 break; 21 } 22 } 23 }
天然,你會發現這個設計很是的糟糕。 由於它根本沒有爲將來的需求變動提供最起碼的擴展。設計
若是你的設計結果是這樣, 那麼當你爲目不暇接的需求變動而焦頭爛額的時候, 你可能更但願讓這份設計到它應該去的地方, 就是桌面的回收站。code
仔細分析這段代碼, 它實際上是一種最古老的面向結構的設計。 若是你要播放的不只僅是 mp3 和 wav,你會不斷增長相應地播放方法, 而後讓 switch 子句愈來愈長, 直至達到你視線看不到的地步。orm
好吧,咱們先來體驗對象的精神。根據OOP的思想,咱們應該吧mp3和wav看做是一個隊裏的對象,那麼是這樣嗎:視頻
1 public class MP3 2 { 3 public void Play() 4 { 5 MessageBox.Show("Play the mp3 file."); 6 } 7 } 8 public class WAV 9 { 10 public void Play() 11 { 12 MessageBox.Show("Play the wav file."); 13 } 14 }
好樣的,你已經知道怎麼創建對象了。更可喜的是,你在不知不覺中應用了重構的方法,把原來那個垃圾設計中的方法名字改成了統一的Play()方法。你在後面的設計中,會發現這樣更名是多麼的關鍵!
但彷佛你並無擊中要害,以如今的方式去更改MediaPlayer的代碼,實質並無多大的變化。
既然mp3和wav都屬於音頻文件,他們都具備音頻文件的共性,爲何不爲他們創建一個共同的父類呢:
1 public class AudioMedia 2 { 3 public void Play() 4 { 5 MessageBox.Show("Play the AudioMedia file."); 6 } 7 }
如今咱們引入了繼承的思想, OOP 也算是象模象樣了。
得意之餘, 仍是認真分析現實世界吧。 其實在現實生活中, 咱們播放的只會是某種具體類型的音頻文件, 所以這個AudioMedia 類並無實際使用的狀況。對應在設計中, 就是: 這個類永遠不會被實例化。
因此, 還得動一下手術, 將其改成抽象類。 好了, 如今的代碼有點 OOP 的感受了:
1 public abstract class AudioMedia 2 { 3 public abstract void Play(); 4 } 5 public class MP3 : AudioMedia 6 { 7 public override void Play() 8 { 9 MessageBox.Show("Play the mp3 file."); 10 } 11 } 12 public class WAV : AudioMedia 13 { 14 public override void Play() 15 { 16 MessageBox.Show("Play the wav file."); 17 } 18 } 19 public class MediaPlayer 20 { 21 public void Play(AudioMedia media) 22 { 23 media.Play(); 24 } 25 }
看看如今的設計, 即知足了類之間的層次關係, 同時又保證了類的最小化原則, 更利於擴展(到這裏,你會發現 play 方法名改得多有必要)。
即便你如今又增長了對 WMA 文件的播放, 只須要設計 WMA 類, 並繼承 AudioMedia,重寫 Play 方法就能夠了, MediaPlayer類對象的 Play 方法根本不用改變。是否是到此就該畫上圓滿的句號呢?
而後刁鑽的客戶是永遠不會知足的, 他們在抱怨這個媒體播放器了。由於他們不想在看足球比賽的時候, 只聽到主持人的解說, 他們更渴望看到足球明星在球場奔跑的英姿。
也就是說, 他們但願你的媒體播放器可以支持視頻文件。你又該痛苦了, 由於在更改硬件設計的同時, 原來的軟件設計結構彷佛出了問題。 由於視頻文件和音頻文件有不少不一樣的地方, 你可不能偷懶, 讓視頻文件對象認音頻文件做父親啊。
你須要爲視頻文件設計另外的類對象了, 假設咱們支持 RM 和 MPEG 格式的視頻:
1 public abstract class VideoMedia 2 { 3 public abstract void Play(); 4 } 5 public class RM : VideoMedia 6 { 7 public override void Play() 8 { 9 MessageBox.Show("Play the rm file."); 10 } 11 } 12 public class MPEG : VideoMedia 13 { 14 public override void Play() 15 { 16 MessageBox.Show("Play the mpeg file. "); 17 } 18 }
糟糕的是, 你不能一勞永逸地享受原有的 MediaPlayer 類了。 由於你要播放的 RM 文件並非 AudioMedia 的子類。
不過不用着急, 由於接口 這個利器你尚未用上(雖然你也能夠用抽象類, 但在 C#裏只支持類的單繼承)。雖然視頻和音頻格式不一樣, 別忘了, 他們都是媒體中的一種,不少時候, 他們有許多類似的功能, 好比播放。
根據接口的定義,你徹底能夠將相同功能的一系列對象實現同一個接口:
1 public interface IMedia 2 { 3 void Play(); 4 } 5 public abstract class AudioMedia : IMedia 6 { 7 public abstract void Play(); 8 } 9 public abstract class VideoMedia : IMedia 10 { 11 public abstract void Play(); 12 }
再更改一下 MediaPlayer 的設計就 OK 了:
1 public class MediaPlayer 2 { 3 public void Play(IMedia media) 4 { 5 media.Play(); 6 } 7 }
如今能夠總結一下,從 MediaPlayer 類的演變,咱們能夠得出這樣一個結論:在調用類對象的屬性和方法時, 儘可能避免將具體類對象做爲傳遞參數, 而應傳遞其抽象對象, 更好地是傳遞接口, 將實際的調用和具體對象徹底剝離開,這樣能夠提升代碼的靈活性。
不過, 事情並無完。 雖然一切看起來都很完美了, 但咱們忽略了這個事實, 就是忘記了 MediaPlayer 的調用者。
還記得文章最開始的 switch 語句嗎? 看起來咱們已經很是漂亮地除掉了這個煩惱。事實上,我在這裏玩了一個詭計, 將 switch 語句延後了。雖然在 MediaPlayer中, 代碼顯得乾淨利落, 其實煩惱只不過是轉嫁到了 MediaPlayer 的調用者那裏。
例如, 在主程序界面中:
1 public void BtnPlay_Click(object sender, EventArgs e) 2 { 3 IMedia media; 4 switch (cbbMediaType.SelectItem.ToString().ToLower()) 5 { 6 case ("mp3"): 7 media = new MP3(); 8 break; 9 case ("wav"): 10 media = new WAV(); 11 break; 12 //其它類型略; 13 } 14 MediaPlayer player = new MediaPlayer(); 15 player.Play(media); 16 }
用戶經過選擇 cbbMediaType 組合框的選項,決定播放哪種文件, 而後單擊 Play 按鈕執行。如今該設計模式粉墨登場了,這種根據不一樣狀況建立不一樣類型的方式, 工廠模式是最拿手的。
先看看咱們的工廠須要生產哪些產品呢 ? 雖然這裏有兩種不一樣類型的媒體AudioMedia 和 VideoMedia(之後可能更多), 但它們同時又都實現 IMedia 接口,因此咱們能夠將其視爲一種產品,用工廠方法模式就能夠了。
首先是工廠接口:
1 public interface IMediaFactory 2 { 3 IMedia CreateMedia(); 4 }
而後爲每種媒體文件對象搭建一個工廠, 並統一實現工廠接口:
1 public class MP3MediaFactory : IMediaFactory 2 { 3 public IMedia CreateMedia() 4 { 5 return new MP3(); 6 } 7 } 8 public class RMMediaFactory : IMediaFactory 9 { 10 public IMedia CreateMedia() 11 { 12 return new RM(); 13 } 14 } 15 //其它工廠略;
寫到這裏, 也許有人會問, 爲何不直接給 AudioMedia 和 VideoMedia 類搭建工廠呢?
很簡單, 由於在 AudioMedia 和 VideoMedia 中, 分別還有不一樣的類型派生, 若是爲它們搭建工廠, 則在 CreateMedia()方法中, 仍然要使用 Switch語句。並且既然這兩個類都實現了 IMedia接口 ,能夠認爲是一種類型, 爲何還要那麼麻煩去請動抽象工廠模式, 來生成兩類產品呢?
可能還會有人問, 即便你使用這種方式, 那麼在判斷具體建立哪一個工廠的時候, 不是也要用到 switch 語句嗎?
我認可這種見解是對的。不過使用工廠模式, 其直接好處並不是是要解決 switch 語句的難題, 而是要延遲對象的生成, 以保證的代碼的靈活性。 固然,我還有最後一招殺手鐗沒有使出來,到後面你會發現, switch 語句其實會徹底消失。還有一個問題, 就是真的有必要實現 AudioMedia 和 VideoMedia 兩個抽象類嗎? 讓其子類直接實現接口 不更簡單? 對於本文提到的需求,我想你是對的, 但不排除 AudioMedia 和VideoMedia 它們還會存在區別。
例如音頻文件只須要提供給聲卡的接口, 而視頻文件還須要提供給顯卡的接口。 若是讓 MP三、 WAV、 RM、 MPEG 直接實現 IMedia 接口, 而不經過AudioMedia 和 VideoMedia, 在知足其它需求的設計上也是不合理的。 固然這已經不包括在本文的範疇了。
如今主程序界面發生了稍許的改變:
public void BtnPlay_Click(object sender, EventArgs e) { IMediaFactory factory = null; switch (cbbMediaType.SelectItem.ToString().ToLower()) { case ("mp3"): factory = new MP3MediaFactory(); break; case ("wav"): factory = new WAVMediaFactory(); break; //其餘類型略; } MediaPlayer player = new MediaPlayer(); player.Play(factory.CreateMedia()); }
寫到這裏, 咱們再回過頭來看 MediaPlayer 類。
這個類中,實現了 Play 方法, 並根據傳遞的參數, 調用相應媒體文件的 Play 方法。 在沒有工廠對象的時候, 看起來這個類對象運行得很好。
若是是做爲一個類庫或組件設計者來看, 他提供了這樣一個接口, 供主界面程序員調用。 然而在引入工廠模式後, 在裏面使用 MediaPlayer 類已經多餘了。
因此,咱們要記住的是, 重構並不只僅是往原來的代碼添加新的內容。 當咱們發現一些沒必要要的設計時, 還須要果斷地刪掉這些冗餘代碼。
1 public void BtnPlay_Click(object sender, EventArgs e) 2 { 3 IMediaFactory factory = null; 4 switch (cbbMediaType.SelectItem.ToString().ToLower()) 5 { 6 case ("mp3"): 7 factory = new MP3MediaFactory(); 8 break; 9 case ("wav"): 10 factory = new WAVMediaFactory(); 11 break; 12 //其餘類型略; 13 } 14 IMedia media = factory.CreateMedia(); 15 media.Play(); 16 }
若是你在最開始沒有體會到 IMedia 接口 的好處, 在這裏你應該已經明白了。咱們在工廠中用到了該接口; 而在主程序中, 仍然要使用該接口 。使用接口有什麼好處?那就是你的主程序能夠在沒有具體業務類的時候, 一樣能夠編譯經過。所以, 即便你增長了新的業務,你的主程序是不用改動的。
不過, 如今看起來,這個不用改動主程序的理想, 依然沒有完成。看到了 嗎?在BtnPlay_Click()中, 依然用 new 建立了一些具體類的實例。
若是沒有徹底和具體類分開, 一旦更改了具體類的業務,例如增長了新的工廠類, 仍然須要改變主程序, 況且討厭的 switch語句仍然存在, 它好像是翅膀上滋生的毒瘤, 提示咱們, 雖然翅膀已經從僵冷的世界裏復活,但這雙翅膀仍是有病的, 並不能正常地飛翔。
是使用配置文件的時候了。咱們能夠把每種媒體文件類類型的相應信息放在配置文件中, 而後根據配置文件來選擇建立具體的對象。 而且,這種建立對象的方法將使用反射來完成。
首先, 建立配置文件:
1 <appSettings> 2 <add key="mp3" value="WingProject.MP3Factory" /> 3 <add key="wav" value="WingProject.WAVFactory" /> 4 <add key="rm" value="WingProject.RMFactory" /> 5 <add key="mpeg" value="WingProject. MPEGFactory" /> 6 </appSettings>
而後, 在主程序界面的 Form_Load 事件中, 讀取配置文件的 全部 key 值, 填充cbbMediaType 組合框控件:
1 public void Form_Load(object sender, EventArgs e) 2 { 3 cbbMediaType.Items.Clear(); 4 foreach (string key in ConfigurationSettings.AppSettings.AllKeys) 5 { 6 cbbMediaType.Item.Add(key); 7 } 8 cbbMediaType.SelectedIndex = 0; 9 }
最後, 更改主程序的 Play 按鈕單擊事件:
1 public void BtnPlay_Click(object sender, EventArgs e) 2 { 3 string mediaType = cbbMediaType.SelectItem.ToString().ToLower(); 4 string factoryDllName = ConfigurationSettings.AppSettings[mediaType].ToString(); 5 //MediaLibray爲引用的媒體文件及工廠的程序集; 6 IMediaFactory factory = (IMediaFactory)Activator.CreateInstance 7 ("MediaLibrary", factoryDllName).Unwrap(); 8 IMedia media = factory.CreateMedia(); 9 media.Play(); 10 }
如今鳥兒的翅膀不只僅復活,有了能夠飛的能力;同時咱們還賦予這雙翅膀更強的功能,它能夠飛得更高, 飛得更遠!享受自由飛翔的愜意吧。
設想一下, 若是咱們要增長某種媒體文件的播放功能, 如 AVI文件。 那麼, 咱們只須要在原來的業務程序集中建立 AVI 類, 並實現 IMedia 接口 , 同時繼承 VideoMedia 類。
另外在工廠業務中建立 AVIMediaFactory 類, 並實現 IMediaFactory 接口。
假設這個新的工廠類型爲 WingProject.AVIFactory, 則在配置文件中添加以下一行:
<add key="avi" value="WingProject.AVIFactory" />
而主程序呢? 根本不須要作任何改變, 甚至不用從新編譯,這雙翅膀照樣能夠自 如地飛行!
《設計之道》學習筆記