從實例談OOP、工廠模式和重構

 

  有了翅膀才能飛, 欠缺靈活的代碼就象凍壞了翅膀的鳥兒。不能飛翔,就少了幾許靈動的氣韻。咱們須要給代碼帶去溫暖的陽光, 讓僵冷的翅膀從新飛起來。程序員

  結合實例, 經過應用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     }
提取MP3和WAV類

 

  好樣的,你已經知道怎麼創建對象了。更可喜的是,你在不知不覺中應用了重構的方法,把原來那個垃圾設計中的方法名字改成了統一的Play()方法。你在後面的設計中,會發現這樣更名是多麼的關鍵!

  但彷佛你並無擊中要害,以如今的方式去更改MediaPlayer的代碼,實質並無多大的變化。

 

  既然mp3和wav都屬於音頻文件,他們都具備音頻文件的共性,爲何不爲他們創建一個共同的父類呢:

1     public class AudioMedia
2     {
3         public void Play()
4         {
5             MessageBox.Show("Play the AudioMedia file.");
6         }
7     }
提取父類AudioMedia.cs

 

  如今咱們引入了繼承的思想, 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" />

 

  而主程序呢? 根本不須要作任何改變, 甚至不用從新編譯,這雙翅膀照樣能夠自 如地飛行!

 

 

 

《設計之道》學習筆記

相關文章
相關標籤/搜索