`
zsw_sh
  • 浏览: 13190 次
文章分类
社区版块
存档分类
最新评论

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

 
阅读更多

【程序乐园】
有了翅膀才能飞,欠缺灵活的代码就象冻坏了翅膀的鸟儿。不能飞翔,就少了几许灵动的气韵。

我们需要给代码带去温暖的阳光,让僵冷的翅膀重新飞起来。结合实例,通过应用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的代码,实质并没有多大的变化。
好样的,你已经知道怎么建立对象了。更可喜的是,你在不知不觉中应用了重构的方法,把原来那个垃圾设计中的方法名字改为了统一的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();     
} 

用户通过选择cbbMediaType组合框的选项,决定播放哪一种文件,然后单击Play按钮执行。
现在该设计模式粉墨登场了,这种根据不同情况创建不同类型的方式,工厂模式是最拿手的。先看看我们的工厂需要生产哪些产品呢?虽然这里有两种不同类型的媒体 AudioMedia和VideoMedia(以后可能更多),但它们同时又都实现IMedia接口,所以我们可以将其视为一种产品,用工厂方法模式就可以了。首先是工厂接口:

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();              
         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();              
          case ("wav"): 
             factory = new WAVMediaFactory();              
             break;//其他类型略;     
    } 
    IMedia media = factory.CreateMedia();     
}    

如果你在最开始没有体会到IMedia接口的好处,在这里你应该已经明白了。我们在工厂中用到了该接口;而在主程序中,仍然要使用该接口。使用接口有什么好处?那就是你的主程序可以在没有具体业务类的时候,同样可以编译通过。因此,即使你增加了新的业务,你的主程序是不用改动的。
不过,现在看起来,这个不用改动主程序的理想,依然没有完成。看到了吗?在BtnPlay_Click()中,依然用new创建了一些具体类的实例。如果没有完全和具体类分开,一旦更改了具体类的业务,例如增加了新的工厂类,仍然需要改变主程序,何况讨厌的switch语句仍然存在,它好像是翅膀上滋生的毒瘤,提示我们,虽然翅膀已经从僵冷的世界里复活,但这双翅膀还是有病的,并不能正常地飞翔。

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

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

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

飞得更高,飞得更远!
享受自由飞翔的惬意吧。设想一下,如果我们要增加某种媒体文件的播放功能,如AVI文件。那么,我们只需要在原来的业务程序集中创建AVI类,并实现 IMedia接口,同时继承VideoMedia类。另外在工厂业务中创建AVIMediaFactory类,并实现IMediaFactory接口。假设这个新的工厂类型为WingProject.AVIFactory,则在配置文件中添加如下一行: 。
而主程序呢?根本不需要做任何改变,甚至不用重新编译,这双翅膀照样可以自如地飞行!

<script type="text/javascript"> $(function () { $('pre.prettyprint code').each(function () { var lines = $(this).text().split('\n').length; var $numbering = $('<ul/>').addClass('pre-numbering').hide(); $(this).addClass('has-numbering').parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($('<li/>').text(i)); }; $numbering.fadeIn(1700); }); }); </script>
分享到:
评论

相关推荐

    从实例谈面向对象编程、工厂模式和重构

    面向对象编程(OOP)是程序设计的一个核心思想,它通过使用对象、类、继承、多态和封装等概念来设计应用程序。OOP的核心优势在于代码的重用性、模块化以及维护性。 工厂模式是一种创建型设计模式,它提供了一种创建...

    delphi面向对象编程思想

    Delphi支持面向对象编程(OOP)的概念,并且通过其丰富的类库VCL(Visual Component Library)为开发者提供了构建Windows应用程序的强大工具。 #### 二、面向对象编程的基本概念 面向对象编程的核心思想是将数据和...

    《面向对象的程序设计》期末试卷(A)答案.pdf

    类(Class)是面向对象编程的基本单元,它是一个蓝图,描述了创建对象时共有的属性和方法。对象(Object)是类的实例,具有类定义的属性和方法的副本。类的属性和方法称为成员(Members)。 2. 封装 封装...

    基于java模式下的编程要诀 代码规范 面向对象

    面向对象编程强调类的封装、继承和多态性,这三大特性是理解OOP的关键。 1. **面向对象编程基础** - **封装**:封装是将数据和操作数据的方法绑定在一起,形成一个独立的对象。这样可以隐藏对象内部细节,提供公共...

    Python面向对象编程详解及其实战应用

    适用于具备一定编程经验且有意深入掌握面向对象编程技术的专业人士或自学学员。 适用场景主要涵盖个人技能提升、团队分享交流及项目重构优化阶段,旨在加深理解并有效应用于现实世界的问题中,提升代码质量和项目的...

    Java面向对象编程深入解析与实战案例

    内容概要:本文全面讲解了Java面向对象编程的核心概念和技术,包括类与对象的创建与使用,封装、继承与多态性的实现技巧及其背后的原则,同时提供了大量实战样例,演示从简单类的设计到继承、抽象类和接口的具体应用...

    浅谈OOP之uml设计模式

    面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式,它以对象为中心,强调数据和操作数据的方法,通过封装、继承和多态等特性来实现软件的模块化和可扩展性。在OOP中,UML(Unified Modeling ...

    Delphi面向对象编程思想

    面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式,它基于“对象”的概念,将数据和操作这些数据的方法封装在一起。在Delphi这种强大的集成开发环境中,面向对象编程是其核心特性之一,它使得...

    深入PHP:面向对象、模式与实践(第三版)高清PDF和完整源码

    1. **面向对象编程(OOP)**: - **类与对象**:书中首先介绍了类作为对象的蓝图,以及如何通过类创建实例化对象。 - **封装**:讨论了如何使用访问修饰符(public, private, protected)来保护数据,实现信息隐藏...

    Carnegie教程-ssd3(面向对象编程与设计)

    面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式,它基于“对象”的概念,将数据和操作数据的方法封装在一起。Carnegie Mellon University(卡内基梅隆大学)的SSD3教程深入讲解了这一主题,...

    JAVA面向对象基础

    面向对象编程(Object-Oriented Programming,简称OOP)是一种重要的编程范式,其核心在于将现实世界中的对象及其属性和行为映射到软件系统中,从而使得程序更加直观、易于理解和维护。OOP的发展历程经历了几个关键...

    西工大面向对象设计与编程实验

    - **面向对象编程实践**:创建和实例化类,使用继承、多态实现功能模块化。 - **文件I/O操作**:学习如何读写文件,理解流的概念。 - **异常处理**:在实践中应用异常处理,编写健壮的代码。 4. **实验技巧与...

    面向对象软件测试资料合集

    面向对象编程(Object-Oriented Programming, OOP)是一种编程范式,它通过类和对象来组织代码。类是具有属性(数据成员)和方法(行为)的模板,而对象是类的实例。在面向对象设计中,我们需要考虑继承、封装和多...

    IBM面向对象软件开发和过程

    首先,面向对象编程(OOP)的基础是类和对象。类是创建对象的模板,它定义了对象的属性(数据成员)和行为(方法)。对象则是类的实例,具有类所定义的特性和功能。在实际开发过程中,开发者会根据需求定义合适的类...

    高清-面向对象葵花宝典 思想、技巧与实践(PDF带目录)

    面向对象编程(Object-Oriented Programming,简称OOP)是一种广泛应用于现代软件开发中的编程范式,它基于“对象”的概念,将数据和操作数据的方法封装在一起,以实现代码的模块化和重用性。《高清-面向对象葵花...

    深入PHP面向对象模式与实践

    面向对象编程(OOP)的核心在于类和对象。类是创建对象的模板,它定义了一组属性(数据成员)和方法(成员函数)。在PHP中,我们可以通过`class`关键字来定义一个类,通过`new`关键字实例化一个对象。类的属性用于...

    java面向对象游戏

    在编程世界中,Java是一种广泛使用的面向对象编程(OOP)语言,因其强大的功能和跨平台性而备受青睐。在这个“java面向对象游戏”的项目中,我们看到开发者使用了Java OOP思想来实现一个猜拳游戏,这为初学者提供了...

    深入浅出面向对象分析与设计(中文版)

    面向对象编程(OOP)是一种编程范式,它基于“对象”的概念,每个对象都包含数据(属性)和操作这些数据的方法(行为)。在Java和C++这两种语言中,OOP都是基础且重要的部分。本书可能会涵盖以下几个核心知识点: 1...

Global site tag (gtag.js) - Google Analytics