物件導向設計思想(C#)

來源:互聯網
上載者:User

有了翅膀才能飛,欠缺靈活的代碼就象凍壞了翅膀的鳥兒。不能飛翔,就少了幾許靈動的氣韻。我們需要給代碼帶去溫暖的陽光,

讓僵冷的翅膀重新飛起來。結合執行個體,通過應用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介面,

所以我們可以將其視為一種產品,用Factory 方法模式就可以了。首先是工廠介面:

public interface IMediaFactory

{

   IMedia CreateMedia();

}

然後為每種媒體檔案對象搭建一個工廠,並統一實現工廠介面:

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它們還會存在區別。例如音頻檔案只需要提供給音效卡的介面,

而視頻檔案還需要提供給顯卡的介面。如果讓MP3、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語句仍然存在,

它好像是翅膀上滋生的毒瘤,提示我們,雖然翅膀已經從僵冷的世界裡複活,但這雙翅膀還是有病的,並不能正常地飛翔。

是使用設定檔的時候了。我們可以把每種媒體檔案類類型的相應資訊放在設定檔中,然後根據設定檔來選擇建立具體的對象。

並且,這種建立對象的方法將使用反射來完成。首先,建立設定檔:

<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();

//MediaLibray為引用的媒體檔案及工廠的程式集;

IMediaFactory factory = (IMediaFactory)Activator.CreateInstance("MediaLibrary",factoryDllName).Unwrap();

IMedia media = factory.CreateMedia();

media.Play();

}

現在鳥兒的翅膀不僅僅複活,有了可以飛的能力;同時我們還賦予這雙翅膀更強的功能,它可以飛得更高,飛得更遠!

享受自由飛翔的愜意吧。設想一下,如果我們要增加某種媒體檔案的播放功能,如AVI檔案。

那麼,我們只需要在原來的業務程式集中建立AVI類,並實現IMedia介面,同時繼承VideoMedia類。

另外在工廠業務中建立AVIMediaFactory類,並實現IMediaFactory介面。假設這個新的工廠類型為WingProject.AVIFactory,

則在設定檔中添加如下一行:

<add key="AVI" value="WingProject.AVIFactory" />。

而主程式呢?根本不需要做任何改變,甚至不用重新編譯,這雙翅膀照樣可以自如地飛行!

本文樣本原始碼,請點擊這裡下載。

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.