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