让僵冷的翅膀飞起来—从实例谈OOP、工厂模式和重构[by Wayfarer]
有了翅膀才能飛,欠缺靈活的代碼就象凍壞了翅膀的鳥(niǎo)兒。不能飛翔,就少了幾許靈動(dòng)的氣韻。我們需要給代碼帶去溫暖的陽(yáng)光,讓僵冷的翅膀重新飛起來(lái)。結(jié)合實(shí)例,通過(guò)應(yīng)用OOP、設(shè)計(jì)模式和重構(gòu),你會(huì)看到代碼是怎樣一步一步復(fù)活的。
為了更好的理解設(shè)計(jì)思想,實(shí)例盡可能簡(jiǎn)單化。但隨著需求的增加,程序?qū)⒃絹?lái)越復(fù)雜。此時(shí)就有修改設(shè)計(jì)的必要,重構(gòu)和設(shè)計(jì)模式就可以派上用場(chǎng)了。最后當(dāng)設(shè)計(jì)漸趨完美后,你會(huì)發(fā)現(xiàn),即使需求不斷增加,你也可以神清氣閑,不用為代碼設(shè)計(jì)而煩惱了。
假定我們要設(shè)計(jì)一個(gè)媒體播放器。該媒體播放器目前只支持音頻文件mp3和wav。如果不談設(shè)計(jì),設(shè)計(jì)出來(lái)的播放器可能很簡(jiǎn)單:
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;????????????
????? }?????
?? }
}
自然,你會(huì)發(fā)現(xiàn)這個(gè)設(shè)計(jì)非常的糟糕。因?yàn)樗緵](méi)有為未來(lái)的需求變更提供最起碼的擴(kuò)展。如果你的設(shè)計(jì)結(jié)果是這樣,那么當(dāng)你為應(yīng)接不暇的需求變更而焦頭爛額的時(shí)候,你可能更希望讓這份設(shè)計(jì)到它應(yīng)該去的地方,就是桌面的回收站。仔細(xì)分析這段代碼,它其實(shí)是一種最古老的面向結(jié)構(gòu)的設(shè)計(jì)。如果你要播放的不僅僅是mp3和wav,你會(huì)不斷地增加相應(yīng)地播放方法,然后讓switch子句越來(lái)越長(zhǎng),直至達(dá)到你視線看不到的地步。
好吧,我們先來(lái)體驗(yàn)對(duì)象的精神。根據(jù)OOP的思想,我們應(yīng)該把mp3和wav看作是一個(gè)獨(dú)立的對(duì)象。那么是這樣嗎?
public class MP3
{
?? public void Play()
?? {
?????? MessageBox.Show("Play the mp3 file.");
?? }
}
public class WAV
{
?? public void Play()
?? {
?????? MessageBox.Show("Play the wav file.");
?? }
}
好樣的,你已經(jīng)知道怎么建立對(duì)象了。更可喜的是,你在不知不覺(jué)中應(yīng)用了重構(gòu)的方法,把原來(lái)那個(gè)垃圾設(shè)計(jì)中的方法名字改為了統(tǒng)一的Play()方法。你在后面的設(shè)計(jì)中,會(huì)發(fā)現(xiàn)這樣改名是多么的關(guān)鍵!但似乎你并沒(méi)有擊中要害,以現(xiàn)在的方式去更改MediaPlayer的代碼,實(shí)質(zhì)并沒(méi)有多大的變化。
既然mp3和wav都屬于音頻文件,他們都具有音頻文件的共性,為什么不為它們建立一個(gè)共同的父類呢?
public class AudioMedia
{
?? public void Play()
?? {
?????? MessageBox.Show("Play the AudioMedia file.");
?? }
}
現(xiàn)在我們引入了繼承的思想,OOP也算是象模象樣了。得意之余,還是認(rèn)真分析現(xiàn)實(shí)世界吧。其實(shí)在現(xiàn)實(shí)生活中,我們播放的只會(huì)是某種具體類型的音頻文件,因此這個(gè)AudioMedia類并沒(méi)有實(shí)際使用的情況。對(duì)應(yīng)在設(shè)計(jì)中,就是:這個(gè)類永遠(yuǎn)不會(huì)被實(shí)例化。所以,還得動(dòng)一下手術(shù),將其改為抽象類。好了,現(xiàn)在的代碼有點(diǎn)OOP的感覺(jué)了:
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();
?? }
}
看看現(xiàn)在的設(shè)計(jì),即滿足了類之間的層次關(guān)系,同時(shí)又保證了類的最小化原則,更利于擴(kuò)展(到這里,你會(huì)發(fā)現(xiàn)play方法名改得多有必要)。即使你現(xiàn)在又增加了對(duì)WMA文件的播放,只需要設(shè)計(jì)WMA類,并繼承AudioMedia,重寫(xiě)Play方法就可以了,MediaPlayer類對(duì)象的Play方法根本不用改變。
是不是到此就該畫(huà)上圓滿的句號(hào)呢?然后刁鉆的客戶是永遠(yuǎn)不會(huì)滿足的,他們?cè)诒г惯@個(gè)媒體播放器了。因?yàn)樗麄儾幌朐诳醋闱虮荣惖臅r(shí)候,只聽(tīng)到主持人的解說(shuō),他們更渴望看到足球明星在球場(chǎng)奔跑的英姿。也就是說(shuō),他們希望你的媒體播放器能夠支持視頻文件。你又該痛苦了,因?yàn)樵诟挠布O(shè)計(jì)的同時(shí),原來(lái)的軟件設(shè)計(jì)結(jié)構(gòu)似乎出了問(wèn)題。因?yàn)橐曨l文件和音頻文件有很多不同的地方,你可不能偷懶,讓視頻文件對(duì)象認(rèn)音頻文件作父親啊。你需要為視頻文件設(shè)計(jì)另外的類對(duì)象了,假設(shè)我們支持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類了。因?yàn)槟阋シ诺腞M文件并不是AudioMedia的子類。
不過(guò)不用著急,因?yàn)榻涌谶@個(gè)利器你還沒(méi)有用上(雖然你也可以用抽象類,但在C#里只支持類的單繼承)。雖然視頻和音頻格式不同,別忘了,他們都是媒體中的一種,很多時(shí)候,他們有許多相似的功能,比如播放。根據(jù)接口的定義,你完全可以將相同功能的一系列對(duì)象實(shí)現(xiàn)同一個(gè)接口:
public interface IMedia
{
?? void Play();
}
public abstract class AudioMedia:IMedia
{
?? public abstract void Play();
}
public abstract class VideoMedia:IMedia
{
?? public abstract void Play();
}
再更改一下MediaPlayer的設(shè)計(jì)就OK了:
public class MediaPlayer
{?
?? public void Play(IMedia media)
?? {?????
?????? media.Play();
?? }
}
現(xiàn)在可以總結(jié)一下,從MediaPlayer類的演變,我們可以得出這樣一個(gè)結(jié)論:在調(diào)用類對(duì)象的屬性和方法時(shí),盡量避免將具體類對(duì)象作為傳遞參數(shù),而應(yīng)傳遞其抽象對(duì)象,更好地是傳遞接口,將實(shí)際的調(diào)用和具體對(duì)象完全剝離開(kāi),這樣可以提高代碼的靈活性。
不過(guò),事情并沒(méi)有完。雖然一切看起來(lái)都很完美了,但我們忽略了這個(gè)事實(shí),就是忘記了MediaPlayer的調(diào)用者。還記得文章最開(kāi)始的switch語(yǔ)句嗎?看起來(lái)我們已經(jīng)非常漂亮地除掉了這個(gè)煩惱。事實(shí)上,我在這里玩了一個(gè)詭計(jì),將switch語(yǔ)句延后了。雖然在MediaPlayer中,代碼顯得干凈利落,其實(shí)煩惱只不過(guò)是轉(zhuǎn)嫁到了MediaPlayer的調(diào)用者那里。例如,在主程序界面中:
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);
}
用戶通過(guò)選擇cbbMediaType組合框的選項(xiàng),決定播放哪一種文件,然后單擊Play按鈕執(zhí)行。
現(xiàn)在該設(shè)計(jì)模式粉墨登場(chǎng)了,這種根據(jù)不同情況創(chuàng)建不同類型的方式,工廠模式是最拿手的。先看看我們的工廠需要生產(chǎn)哪些產(chǎn)品呢?雖然這里有兩種不同類型的媒體AudioMedia和VideoMedia(以后可能更多),但它們同時(shí)又都實(shí)現(xiàn)IMedia接口,所以我們可以將其視為一種產(chǎn)品,用工廠方法模式就可以了。首先是工廠接口:
public interface IMediaFactory
{
?? IMedia CreateMedia();
}
然后為每種媒體文件對(duì)象搭建一個(gè)工廠,并統(tǒng)一實(shí)現(xiàn)工廠接口:
public class MP3MediaFactory:IMediaFactory
{
?? public IMedia CreateMedia()
?? {
?????? return new MP3();
?? }
}
public class RMMediaFactory:IMediaFactory
{
?? public IMedia CreateMedia()
?? {
?????? return new RM();
?? }
}
//其它工廠略;
寫(xiě)到這里,也許有人會(huì)問(wèn),為什么不直接給AudioMedia和VideoMedia類搭建工廠呢?很簡(jiǎn)單,因?yàn)樵贏udioMedia和VideoMedia中,分別還有不同的類型派生,如果為它們搭建工廠,則在CreateMedia()方法中,仍然要使用Switch語(yǔ)句。而且既然這兩個(gè)類都實(shí)現(xiàn)了IMedia接口,可以認(rèn)為是一種類型,為什么還要那么麻煩去請(qǐng)動(dòng)抽象工廠模式,來(lái)生成兩類產(chǎn)品呢?
可能還會(huì)有人問(wèn),即使你使用這種方式,那么在判斷具體創(chuàng)建哪個(gè)工廠的時(shí)候,不是也要用到switch語(yǔ)句嗎?我承認(rèn)這種看法是對(duì)的。不過(guò)使用工廠模式,其直接好處并非是要解決switch語(yǔ)句的難題,而是要延遲對(duì)象的生成,以保證的代碼的靈活性。當(dāng)然,我還有最后一招殺手锏沒(méi)有使出來(lái),到后面你會(huì)發(fā)現(xiàn),switch語(yǔ)句其實(shí)會(huì)完全消失。
還有一個(gè)問(wèn)題,就是真的有必要實(shí)現(xiàn)AudioMedia和VideoMedia兩個(gè)抽象類嗎?讓其子類直接實(shí)現(xiàn)接口不更簡(jiǎn)單?對(duì)于本文提到的需求,我想你是對(duì)的,但不排除AudioMedia和VideoMedia它們還會(huì)存在區(qū)別。例如音頻文件只需要提供給聲卡的接口,而視頻文件還需要提供給顯卡的接口。如果讓MP3、WAV、RM、MPEG直接實(shí)現(xiàn)IMedia接口,而不通過(guò)AudioMedia和VideoMedia,在滿足其它需求的設(shè)計(jì)上也是不合理的。當(dāng)然這已經(jīng)不包括在本文的范疇了。
現(xiàn)在主程序界面發(fā)生了稍許的改變:
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());
}
寫(xiě)到這里,我們?cè)倩剡^(guò)頭來(lái)看MediaPlayer類。這個(gè)類中,實(shí)現(xiàn)了Play方法,并根據(jù)傳遞的參數(shù),調(diào)用相應(yīng)媒體文件的Play方法。在沒(méi)有工廠對(duì)象的時(shí)候,看起來(lái)這個(gè)類對(duì)象運(yùn)行得很好。如果是作為一個(gè)類庫(kù)或組件設(shè)計(jì)者來(lái)看,他提供了這樣一個(gè)接口,供主界面程序員調(diào)用。然而在引入工廠模式后,在里面使用MediaPlayer類已經(jīng)多余了。所以,我們要記住的是,重構(gòu)并不僅僅是往原來(lái)的代碼添加新的內(nèi)容。當(dāng)我們發(fā)現(xiàn)一些不必要的設(shè)計(jì)時(shí),還需要果斷地刪掉這些冗余代碼。
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();
}
如果你在最開(kāi)始沒(méi)有體會(huì)到IMedia接口的好處,在這里你應(yīng)該已經(jīng)明白了。我們?cè)诠S中用到了該接口;而在主程序中,仍然要使用該接口。使用接口有什么好處?那就是你的主程序可以在沒(méi)有具體業(yè)務(wù)類的時(shí)候,同樣可以編譯通過(guò)。因此,即使你增加了新的業(yè)務(wù),你的主程序是不用改動(dòng)的。
不過(guò),現(xiàn)在看起來(lái),這個(gè)不用改動(dòng)主程序的理想,依然沒(méi)有完成??吹搅藛?#xff1f;在BtnPlay_Click()中,依然用new創(chuàng)建了一些具體類的實(shí)例。如果沒(méi)有完全和具體類分開(kāi),一旦更改了具體類的業(yè)務(wù),例如增加了新的工廠類,仍然需要改變主程序,何況討厭的switch語(yǔ)句仍然存在,它好像是翅膀上滋生的毒瘤,提示我們,雖然翅膀已經(jīng)從僵冷的世界里復(fù)活,但這雙翅膀還是有病的,并不能正常地飛翔。
是使用配置文件的時(shí)候了。我們可以把每種媒體文件類類型的相應(yīng)信息放在配置文件中,然后根據(jù)配置文件來(lái)選擇創(chuàng)建具體的對(duì)象。并且,這種創(chuàng)建對(duì)象的方法將使用反射來(lái)完成。首先,創(chuàng)建配置文件:
<appSettings>
? <add key="mp3" value="WingProject.MP3Factory" />
? <add key="wav" value="WingProject.WAVFactory" />
? <add key="rm" value="WingProject.RMFactory" />
? <add key="mpeg" value="WingProject.MPEGFactory" />
</appSettings>
然后,在主程序界面的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();
}
現(xiàn)在鳥(niǎo)兒的翅膀不僅僅復(fù)活,有了可以飛的能力;同時(shí)我們還賦予這雙翅膀更強(qiáng)的功能,它可以飛得更高,飛得更遠(yuǎn)!
享受自由飛翔的愜意吧。設(shè)想一下,如果我們要增加某種媒體文件的播放功能,如AVI文件。那么,我們只需要在原來(lái)的業(yè)務(wù)程序集中創(chuàng)建AVI類,并實(shí)現(xiàn)IMedia接口,同時(shí)繼承VideoMedia類。另外在工廠業(yè)務(wù)中創(chuàng)建AVIMediaFactory類,并實(shí)現(xiàn)IMediaFactory接口。假設(shè)這個(gè)新的工廠類型為WingProject.AVIFactory,則在配置文件中添加如下一行:
<add key="AVI" value="WingProject.AVIFactory" />。
而主程序呢?根本不需要做任何改變,甚至不用重新編譯,這雙翅膀照樣可以自如地飛行!
本文示例源代碼,請(qǐng)點(diǎn)擊這里下載。
轉(zhuǎn)載于:https://www.cnblogs.com/djshow/archive/2004/12/07/73835.html
總結(jié)
以上是生活随笔為你收集整理的让僵冷的翅膀飞起来—从实例谈OOP、工厂模式和重构[by Wayfarer]的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 信用卡逾期一天会有记录吗?逾期有记录怎么
- 下一篇: 信用卡有逾期还能不能办信用卡?怎么办?