当前位置:中国站长下载文章中心网页编程.NET编程 → 面向对象设计思想(C#)

面向对象设计思想(C#)

减小字体 增大字体 作者:battlerstar  来源:Blog  发布时间:2008-9-1 18:13:20

       有了翅膀才能飞,欠缺灵活的代码就象冻坏了翅膀的鸟儿。不能飞翔,就少了几许灵动的气韵。我们需要给代码带去温暖的阳光, 让僵冷的翅膀重新飞起来。结合实例,通过应用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.");
   }
}

       好样的,你已经知道怎么建立对象了。更可喜的是,你在不知不觉中应用了重构的方法,把原来那个垃圾设计中的方法名字改为了 统一的Play()方法。你在后面的设计中,会发现这样改名是多么的关键!但似乎你并没有击中要害, 以现在的方式去更改MediaPlayer的代码,实质并没有多大的变化。
       
既然mp3和wav都属于音频文件,他们都具有音频文件的共性,为什么不为它们建立一个共同的父类呢?

public class AudioMedia
{
   public void Play()
   {
       MessageBox.Show("Play the AudioMedia file.");
   }
}

        现在我们引入了继承的思想,OOP也算是象模象样了。得意之余,还是认真分析现实世界吧。其实在现实生活中,我们播放的只会是某种具体类型的音频文件,因此这个AudioMedia类并没有实际使用的情况。对应在设计中,就是:这个类永远不会被实例化。所以,还得动一下手术,将其改为抽象类。好了,现在的代码有点OOP的感觉了:

        看看现在的设计,即满足了类之间的层次关系,同时又保证了类的最小化原则,更利于扩展(到这里,你会发现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(以后可能更多),但它们同时又都实现IMedi

[1] [2]  下一页