有了翅膀才能飞, 欠缺灵活的代码就象冻坏了翅膀的鸟儿。不能飞翔,就少了几许灵动的气韵。咱们须要给代码带去温暖的阳光, 让僵冷的翅膀从新飞起来。程序员
结合实例, 经过应用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 }
好样的,你已经知道怎么创建对象了。更可喜的是,你在不知不觉中应用了重构的方法,把原来那个垃圾设计中的方法名字改成了统一的Play()方法。你在后面的设计中,会发现这样更名是多么的关键!
但彷佛你并无击中要害,以如今的方式去更改MediaPlayer的代码,实质并无多大的变化。
既然mp3和wav都属于音频文件,他们都具备音频文件的共性,为何不为他们创建一个共同的父类呢:
1 public class AudioMedia 2 { 3 public void Play() 4 { 5 MessageBox.Show("Play the AudioMedia file."); 6 } 7 }
如今咱们引入了继承的思想, 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" />
而主程序呢? 根本不须要作任何改变, 甚至不用从新编译,这双翅膀照样能够自 如地飞行!
《设计之道》学习笔记