从实例谈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" />

 

  而主程序呢? 根本不须要作任何改变, 甚至不用从新编译,这双翅膀照样能够自 如地飞行!

 

 

 

《设计之道》学习笔记

相关文章
相关标签/搜索