编程思想┊从实例谈面向对象编程(OOP)、工厂模式和重构

有了翅膀才能飞,欠缺灵活的代码就象冻坏了翅膀的鸟儿。不能飞翔,就少了几许灵动的气韵。我们需要给代码带去温暖的阳光,让僵冷的翅膀重新飞起来。结合实例,通过应用OOP、设计模式和重构,你会看到代码是怎样一步一步复活的。

为了更好的理解设计思想,实例尽可能简单化。但随着需求的增加,程序将越来越复杂。此时就有修改设计的必要,重构和设计模式就可以派上用场了。最后当设计渐趋完美后,你会发现,即使需求不断增加,你也可以神清气闲,不用为代码设计而烦恼了。

假定我们要设计一个媒体播放器。该媒体播放器目前只支持音频文件mp3和wav。如果不谈设计,设计出来的播放器可能很简单:

 1 public class MediaPlayer
 2 {
 3    private void PlayMp3()
 4    {
 5       MessageBox.Show("Play the mp3 file.");
 6    }
 7
 8    private void PlayWav()
 9    {
10       MessageBox.Show("Play the wav file.");
11    }
12
13    public void Play(string audioType)
14    {
15       switch (audioType.ToLower())
16       {
17           case ("mp3"):
18              PlayMp3();
19              break;
20           case ("wav"):
21              PlayWav();
22              break;
23       }
24    }
25 }

自然,你会发现这个设计非常的糟糕。因为它根本没有为未来的需求变更提供最起码的扩展。如果你的设计结果是这样,那么当你为应接不暇的需求变更而焦头烂额的时候,你可能更希望让这份设计到它应该去的地方,就是桌面的回收站。仔细分析这段代码,它其实是一种最古老的面向结构的设计。如果你要播放的不仅仅是 mp3和wav,你会不断地增加相应地播放方法,然后让switch子句越来越长,直至达到你视线看不到的地步。

好吧,我们先来体验对象的精神。根据OOP的思想,我们应该把mp3和wav看作是一个独立的对象。那么是这样吗?

 1 public class MP3
 2 {
 3    public void Play()
 4    {
 5        MessageBox.Show("Play the mp3 file.");
 6    }
 7 }
 8
 9 public class WAV
10 {
11    public void Play()
12    {
13        MessageBox.Show("Play the wav file.");
14    }
15 }

好样的,你已经知道怎么建立对象了。更可喜的是,你在不知不觉中应用了重构的方法,把原来那个垃圾设计中的方法名字改为了统一的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
 6 public class MP3:AudioMedia
 7 {
 8    public override void Play()
 9    {
10        MessageBox.Show("Play the mp3 file.");
11    }
12 }
13
14 public class WAV:AudioMedia
15 {
16    public override void Play()
17    {
18        MessageBox.Show("Play the wav file.");
19    }
20 }
21
22 public class MediaPlayer
23 {
24    public void Play(AudioMedia media)
25    {
26        media.Play();
27    }
28 }

看看现在的设计,即满足了类之间的层次关系,同时又保证了类的最小化原则,更利于扩展(到这里,你会发现play方法名改得多有必要)。即使你现在又增加了对WMA文件的播放,只需要设计WMA类,并继承AudioMedia,重写Play方法就可以了,MediaPlayer类对象的Play方法根本不用改变。

是不是到此就该画上圆满的句号呢?然后刁钻的客户是永远不会满足的,他们在抱怨这个媒体播放器了。因为他们不想在看足球比赛的时候,只听到主持人的解说,他们更渴望看到足球明星在球场奔跑的英姿。也就是说,他们希望你的媒体播放器能够支持视频文件。你又该痛苦了,因为在更改硬件设计的同时,原来的软件设计结构似乎出了问题。因为视频文件和音频文件有很多不同的地方,你可不能偷懒,让视频文件对象认音频文件作父亲啊。你需要为视频文件设计另外的类对象了,假设我们支持RM和MPEG格式的视频:

 1 public abstract class VideoMedia
 2 {
 3    public abstract void Play();
 4 }
 5
 6 public class RM:VideoMedia
 7 {
 8    public override void Play()
 9    {
10        MessageBox.Show("Play the rm file.");
11    }
12 }
13
14 public class MPEG:VideoMedia
15 {
16    public override void Play()
17    {
18        MessageBox.Show("Play the mpeg file.");
19    }
20 }

糟糕的是,你不能一劳永逸地享受原有的MediaPlayer类了。因为你要播放的RM文件并不是AudioMedia的子类。

不过不用着急,因为接口这个利器你还没有用上(虽然你也可以用抽象类,但在C#里只支持类的单继承)。虽然视频和音频格式不同,别忘了,他们都是媒体中的一种,很多时候,他们有许多相似的功能,比如播放。根据接口的定义,你完全可以将相同功能的一系列对象实现同一个接口:

 1 public interface IMedia
 2 {
 3    void Play();
 4 }
 5
 6 public abstract class AudioMedia:IMedia
 7 {
 8    public abstract void Play();
 9 }
10
11 public abstract class VideoMedia:IMedia
12 {
13    public abstract void Play();
14 }

再更改一下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     switch (cbbMediaType.SelectItem.ToString().ToLower())
 4     {
 5         IMedia media;
 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 }

//其它工厂略;

写到这里,也许有人会问,为什么不直接给AudioMedia和VideoMedia类搭建工厂呢?很简单,因为在AudioMedia和 VideoMedia中,分别还有不同的类型派生,如果为它们搭建工厂,则在CreateMedia()方法中,仍然要使用Switch语句。而且既然这两个类都实现了IMedia接口,可以认为是一种类型,为什么还要那么麻烦去请动抽象工厂模式,来生成两类产品呢?

可能还会有人问,即使你使用这种方式,那么在判断具体创建哪个工厂的时候,不是也要用到switch语句吗?我承认这种看法是对的。不过使用工厂模式,其直接好处并非是要解决 switch语句的难题,而是要延迟对象的生成,以保证的代码的灵活性。当然,我还有最后一招杀手锏没有使出来,到后面你会发现,switch语句其实会完全消失。

还有一个问题,就是真的有必要实现AudioMedia和VideoMedia两个抽象类吗?让其子类直接实现接口不更简单?对于本文提到的需求,我想你是对的,但不排除AudioMedia和VideoMedia它们还会存在区别。例如音频文件只需要提供给声卡的接口,而视频文件还需要提供给显卡的接口。如果让MP3、WAV、RM、MPEG直接实现IMedia接口,而不通过AudioMedia和VideoMedia,在满足其它需求的设计上也是不合理的。当然这已经不包括在本文的范畴了。

现在主程序界面发生了稍许的改变:

 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     MediaPlayer player = new MediaPlayer();
15     player.Play(factory.CreateMedia());
16 }

写到这里,我们再回过头来看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语句仍然存在,它好像是翅膀上滋生的毒瘤,提示我们,虽然翅膀已经从僵冷的世界里复活,但这双翅膀还是有病的,并不能正常地飞翔。

是使用配置文件的时候了。我们可以把每种媒体文件类类型的相应信息放在配置文件中,然后根据配置文件来选择创建具体的对象。并且,这种创建对象的方法将使用反射来完成。首先,创建配置文件:

然后,在主程序界面的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 IMediaFactory factory = (IMediaFactory)Activator.CreateInstance("MediaLibrary",factoryDllName).Unwrap ();//MediaLibray为引用的媒体文件及工厂的程序集;
6 IMedia media = factory.CreateMedia();
7 media.Play();
8 }

现在鸟儿的翅膀不仅仅复活,有了可以飞的能力;同时我们还赋予这双翅膀更强的功能,它可以飞得更高,飞得更远!

享受自由飞翔的惬意吧。设想一下,如果我们要增加某种媒体文件的播放功能,如AVI文件。那么,我们只需要在原来的业务程序集中创建AVI类,并实现 IMedia接口,同时继承VideoMedia类。另外在工厂业务中创建AVIMediaFactory类,并实现IMediaFactory接口。假设这个新的工厂类型为WingProject.AVIFactory,则在配置文件中添加如下一行:

而主程序呢?根本不需要做任何改变,甚至不用重新编译,这双翅膀照样可以自如地飞行!

文章链接:http://www.iplaysoft.com/oop-factory-reconstruct.html

时间: 2024-09-29 03:29:29

编程思想┊从实例谈面向对象编程(OOP)、工厂模式和重构的相关文章

虚拟世界(代码)--浅谈面向对象编程感触

不知不觉已经来到北京一年了,接触编程也一年了,或许我已经不再是那个连HelloWord都要写了N遍都不理解的低级菜鸟了(因为现在是中级了!!~~依旧是菜鸟) 在之前从来没有接触过编程,也不知道什么面向对象编程,面向过程编程.我的代码人生就是从这面向对象编程开始的. 面向对象编程,接触了C#和Java两种编程语言,感触良多(说实话,我现在已经快记不得老师讲过的有哪些重点了).我根据自己所了解的谈谈这段时间的收获. 一.什么是面向对象编程? 最初,老师告诉我们C#和Java都是面向对象编程的语言.那

从实例谈OOP、工厂模式和重构

有了翅膀才能飞, 欠缺灵活的代码就象冻坏了翅膀的鸟儿.不能飞翔,就少了几许灵动的气韵.我们需要给代码带去温暖的阳光, 让僵冷的翅膀重新飞起来. 结合实例, 通过应用OOP.设计模式和重构,你会看到代码是怎样一步一步复活的. 为了更好的理解设计思想, 实例尽可能简单化. 但随着需求的增加,程序将越来越复杂. 此时就有修改设计的必要, 重构和设计模式就可以派上用场了. 最后当设计渐趋完美后,你会发现, 即使需求不断增加,你也可以神清气闲,不用为代码设计而烦恼了. 假定我们要设计一个媒体播放器. 该媒

面向对象之简单工厂模式

using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Interface { class InterfaceTest { static void Main(string[] args) { #region 测试Cat,Monkey,Bear类 introduction test; for (int i = 1; i<=3; i++) { switch (i)

JS面向对象基础讲解(工厂模式、构造函数模式、原型模式、混合模式、动态原型模)

什么是面向对象?面向对象是一种思想!(废话). 面向对象可以把程序中的关键模块都视为对象,而模块拥有属性及方法.这样我们如果把一些属性及方法封装起来,日后使用将非常方便,也可以避免繁琐重复的工作.接下来将为大家讲解在JS中面向对象的实现.   工厂模式 工厂模式是软件工程领域一种广为人知的设计模式,而由于在ECMAScript中无法创建类,因此用函数封装以特定接口创建对象.其实现方法非常简单,也就是在函数内创建一个对象,给对象赋予属性及方法再将对象返回即可. ? 1 2 3 4 5 6 7 8

浅谈php设计模式(1)---工厂模式

一.接口继承直接调用 先看看这样一段代码: 1 <?php 2 3 interface db{ 4 function conn(); 5 } 6 7 class dbmysql implements db { 8 public function conn(){ 9 echo "连接到了mysql"; 10 } 11 } 12 13 class dbsqlite implements db{ 14 public function conn(){ 15 echo "连接到了

Python3学习之路~6.2 实例演示面向对象编程的好处

首先建一个dog类,实例化为3个dog对象,并让它们都叫. class Dog: def bulk(self): print("xiaohuang:wang wang wang !") d1 = Dog() d2 = Dog() d3 = Dog() d1.bulk() d2.bulk() d3.bulk() # 运行结果: # xiaohuang:wang wang wang ! # xiaohuang:wang wang wang ! # xiaohuang:wang wang w

day10 浅谈面向对象编程

面向对象编程:第一步找名词,名词是问题域中的. 第二步概括名词设计成类.某些名词可以浓缩包含到其它名词中,成为其属性. 第三步找动词,动词也是问题域中的.   第四步概括动词设计成方法.动作的产生往往是对象身上发生的,根据动词动作的产生归纳到所属对象. 第五步根据需求确立方法的参数和返回值.如果在调用的地方,不需要值的返回或者返回的值后面根本用不到,则使用void.反之,如果方法调用的地方需要返回值或者后面其它地方需要用到该方法的结果,则设置相应的返回类型.如果方法中要使用其它地方传进来的值数据

Python 编程核心知识体系-模块|面向对象编程(三)

模块 面向对象编程

js面向对象小结(工厂模式,构造函数,原型方法,继承)

最近过了一遍尼古拉斯泽卡斯的高级程序设计第三版(红皮书)第六章:面向对象程序设计,现在把总结出来的东西和大家分享一下. 主要内容如下: 1.工厂模式 2.构造函数模式 3.原型模式 4.继承 一.工厂模式 工厂模式中的函数中会创建一个对象,最后return这个对象,通过每次调用时传入的参数不同来解决创建多个相似对象的问题. // 工厂模式 function creatPerson(name, age, job) { var o = {}; o.name = name; o.age = age;