1.4 多态的艺术
本节将介绍以下内容:
·什么是多态?
·动态绑定
·品味多态和面向对象
1.4.1 引言
翻开大部头的《韦氏大词典》,关于多态(Polymorphisn)的定义为:可以呈现不同形式的能力或状态。这一术语来源于生物系统,意指同族生物具有的相同特征。而在.NET中,多态指同一操作作用于不同的实例,产生不同运行结果的机制。继承、封装和多态构成面向对象三要素,成就了面向对象编程模式的基础技术机制。
在本节,我们以入情入理的小故事为线索,来展开一次关于多态的循序渐进之旅,在故事的情节中思考多态和面向对象的艺术品质。
1.4.2 问题的抛出
故事开始。
小王的爷爷,开始着迷于电脑这个新鲜玩意儿了,但是老人家面对陌生的屏幕却总是摸不着头脑,各种各样的文件和资料眼花缭乱,老人家却不知道如何打开,这可急坏了身为光荣程序员的小王。为了让爷爷享受高科技带来的便捷与震撼,小王决定自己开发一个万能程序,用来一键式打开常见的计算机资料,例如文档、图片和影音文件等,只需安装一个程序就可以免了其他应用文件的管理,并且使用方便,就暂且称之为万能加载器(FileLoader)吧。
既然是个独立的应用系统,小王就分析了万能加载器应有的几个功能点,小结如下:
·自动加载各种资料,一站式搜索系统常见资料。
·能够打开常见文档类资料,例如txt文件、Word文件、PDF文件、Visio文件等。
·能够打开常见图片资料,例如jpg格式文件、gif格式文件、png格式文件等。
·能够打开常见音频资料和视频资料,例如avi文件、mp3文件等。
·支持简单可用的类型扩展接口,易于实现更多文件类型的加载。
这可真是一个不小的挑战,小王决定利用业余时间逐步地来实现这一伟大的构想,就当成是送给爷爷60岁的寿礼。有了一个令人兴奋的念头,小王怎么都睡不着,半夜按捺不住爬起来,构思了一个基本的系统流程框架,如图1-11所示。
图1-11 万能加载器系统框架图
1.4.3 最初的实现
说干就干,小王按照构思的系统框架,首先构思了可能打开的最常用的文件,并将其设计为一个枚举,这样就可以统一来管理文件的类型了,实现如下:
//可支持文件类型,以文件扩展名划分 enum FileType { doc, //Word文档 pdf, //PDF文档 txt, //文本文档 ppt, //Powerpoint文档 jpg, //jpg格式图片 gif, //gif格式图片 mp3, //mp3音频文件 avi //avi视频文件 }
看着这个初步设想的文件类型枚举,小王暗暗觉得真不少,如果再增加一些常用的文件类型,这个枚举还真是气魄不小呀。
有了要支持的文件类型,小王首先想到的就是实现一个文件类,来代表不同类型的文件资料,具体如下:
class Files { private FileType fileType; public FileType FileType { get { return fileType; } } }
接着小王按照既定思路构建了一个打开文件的管理类,为每种文件实现其具体的打开方式,例如:
class FileManager { //打开Word文档 public void OpenDocFile() { Console.WriteLine("Alibaba, Open the Word file."); } //打开PDF文档 public void OpenPdfFile() { Console.WriteLine("Alibaba, Open the PDF File."); } //打开Jpg文档 public void OpenJpgFile() { Console.WriteLine("Alibaba, Open the Jpg File."); } //打开MP3文档 public void OpenMp3File() { Console.WriteLine("Alibaba, Open the MP3 File."); } }
哎呀,这个长长的单子还在继续往下写:OpenJpgFile、OpenGifFile、OpenMp3File、OpenAviFile……不知到什么时候。
上一步着实让小王步履维艰,下一步的实现更让小王濒临崩溃了,在系统调用端,小王实现的文件加载器是被这样实现的:
class FileClient { public static void Main() { //首先启动文件管理器 FileManager fm = new FileManager(); //看到一堆一堆的电脑资料 IList<Files> files = new List<Files>(); //当前的万能加载器该如何完成工作呢? foreach (Files file in files) { switch(file.FileType) { case FileType.doc: fm.OpenDocFile(); break; case FileType.pdf: fm.OpenPdfFile(); break; case FileType.jpg: fm.OpenJpgFile(); break; case FileType.mp3: fm.OpenMp3File(); break; //……部分省略…… } } } }
完成了文件打开的调用端,一切都好像上了轨道,小王的万能文档器也有了基本的架子,剩下再根据实际需求做些调整即可。小王兴冲冲地将自己的作品拿给爷爷试手,却发现爷爷正在想打开一段rm格式的京剧听听。但是小王的系统还没有支持这一文件格式,没办法只好回去继续修改了。
等到要添加支持新类型的时候,拿着半成品的小王,突然发现自己的系统好像很难再插进一脚,除了添加新的文件支持类型,修改打开文件操作代码,还得在管理类中添加新的支持代码,最后在客户端还要修改相应的操作。小王发现添加新的文件类型,好像把原来的系统整个做了一次大装修,那么下次爷爷那里有了新需求呢,号称万能加载器的作品,应该怎么应付下一次的需求变化呢?这真是噩梦,气喘吁吁的小王,忍不住回头看了看一天的作品,才发现自己好像掉进了深渊,无法回头。勇于探索的小王经过一番深入的分析发现了当前设计的几个重要问题,主要包括:
·需要深度调整客户端,为系统维护带来麻烦,况且我们应该尽量保持客户端的相对稳定。
·Word、PDF、MP3等,都是可以实现的独立对象,整个系统除了有文档管理类,几乎没有面向对象的影子,全部是面向结构和过程的开发方式。
·在实现打开文件程序时,小王发现其实OpenDocFile方法、OpenPDFFile方法以及OpenTxtFile方法有很多可复用的代码,而OpenJpgFile方法和OpenGifFile方法也有很多重复构造的地方。
·由于系统之间没有分割、没有规划,整个系统就像一堆乱麻,几乎不可能完成任何简单的扩展和维护。
·任何修改都会将整个系统洗礼一次,修改遍布全系统的整个代码,并且全部重新编译才行。
·需求变更是结构化设计的大敌,无法轻松完成起码的系统扩展和变更,例如在打开这一操作之外,如果实现删除、重命名等其他操作,对当前的系统来说将是致命的打击。在发生需求多变的今天,必须实现能够灵活扩展和简单变更的设计构思,面向对象是灵活设计的有效手段之一。
1.4.4 多态,救命的稻草
看着经不起考验的系统,经过了短期的郁闷和摸索,小王终于找到了阿里巴巴念动芝麻之门打开的魔咒,这就是:多态。
没错!就是多态,就是面向对象。这是小王痛定思痛后,发出的由衷感慨。小王再接再厉,颠覆了原来的构思,一个新的设计框架应运而生,如图1-12所示。
结合新的框架,比较之前的蹩脚设计,小王提出了新系统的新气象,主要包括以下几个修改:
·将Word、PDF、TXT、JPG、AVI等业务实体抽象为对象,并在每个相应的对象内部来处理本对象类型的文件打开工作,这样各个类型之间的交互操作就被分离出来,这样很好地体现了职责单一原则的目标。
图1-12 万能加载器系统设计
·将各个对象的属性和行为相分离,将文件打开这一行为封装为接口,再由其他类来实现这一接口,有利于系统的扩展同时减少了类与类的依赖。
·将相似的类抽象出公共基类,在基类中实现具有共性的特征,并由子类继承父类的特征,例如Word、PDF、TXT的基类可以抽象为DocLoader;而JPG和GIF的基类可以抽象为ImageLoader,这种实现体现的是面向对象的开放封闭原则:对扩展开放,对修改关闭。如果有新的类型需要扩展,则只需继承合适的基类成员,实现新类型的特征代码即可。
·实现可柔性扩展的接口机制,能够更加简单的实现增加新的文件类型加载程序,也能够很好的扩展打开文件之外的其他操作,例如删除、重命名等修改操作。
·实现在不需要调整原系统,或者很少调整原系统的情况下,进行功能扩展和优化,甚至是无须编译的插件式系统。
下面是具体的实现,首先是通用的接口定义:
interface IFileOpen { void Open(); }
接着定义所有文件类型的公共基类,因为公共的文件基类是不可以实例化的,在此处理为抽象类实现会更好,详细为:
abstract class Files: IFileOpen { private FileType fileType = FileType.doc; public FileType FileType { get { return fileType; } } public abstract void Open(); }
基类Files实现了IFileOpen接口,不过在此仍然定义方法为抽象方法。除了文件打开抽象方法,还可以实现其他的通用文件处理操作,例如文件删除Delete、文件重命名ReName和获取文件路径等。有了文件类型的公共基类,是时候实现其派生类了。经过一定的分析和设计,小王没有马上提供具体的资料类型类,而是对派生类型做了归档,初步实现文件类型、图片类型和媒体类型三个大类,将具体的文件类型进一步做了抽象:
abstract class DocFile: Files { public int GetPageCount() { //计算文档页数 } } abstract class ImageFile : Files { public void ZoomIn() { //放大比例 } public void ZoomOut() { //缩小比例 } }
终于是实现具体资料类的时候了,在此以Word类型为例来说明具体的实现:
class WORDFile : DocFile { public override void Open() { Console.WriteLine("Open the WORD file."); } }
其他类型的实现类似于此,不同之处在于不同的类型有不同Open实现规则,以应对不同资料的打开操作。小王根据架构的设计,同时提供了一个资料管理类来进行资料的统一管理:
class LoadManager { private IList<Files> files = new List<Files>(); public IList<Files> Files { get { return files; } } public void LoadFiles(Files file) { files.Add(file); } //打开所有资料 public void OpenAllFiles() { foreach(IFileOpen file in files) { file.Open(); } } //打开单个资料 public void OpenFile(IFileOpen file) { file.Open(); } //获取文件类型 public FileType GetFileType(string fileName) { //根据指定路径文件返回文件类型 FileInfo fi = new FileInfo(fileName); return (FileType)Enum.Parse(typeof(FileType), fi.Extension); } }
最后,小王实现了简单的客户端,并根据所需进行文件的加载:
class FileClient { public static void Main() { //首先启动文件加载器 LoadManager lm = new LoadManager(); //添加要处理的文件 lm.LoadFiles(new WORDFile()); lm.LoadFiles(new PDFFile()); lm.LoadFiles(new JPGFile()); lm.LoadFiles(new AVIFile()); foreach (Files file in lm.Files) { if (file is 爷爷选择的) //伪代码 { lm.OpenFile(file); } } } }
当然,现在的FileLoader客户端还有很多要完善的工作要做,例如关于文件加载的类型,完全可以定义在配置文件中,并通过抽象工厂模式和反射于运行期动态获取,以避免耦合在客户端。不过基本的文件处理部分已经能够满足小王的预期。
1.4.5 随需而变的业务
爷爷机子上的资料又增加了新的视频文件MPEG,原来的AVI文件都太大了。可是这回根本就没有难倒小王的万能加载器。在电脑前轻松地折腾30分钟后,万能加载器就可以适应新的需求,图1-13所示的是修改的框架设计。
图1-13 万能加载器架构设计调整
按照这个新的设计,小王对系统只需做如下的简单调整,首先是增加处理MPEG文件的类型MPEGFile,并让它继承自MediaFile,实现具体的Open方法即可。
class MPEGFile : MediaFile { public override void Open() { Console.WriteLine("Open the MPEG file."); } }
接着就是添加处理新文件的加载操作,如下:
lm.LoadFiles(new MPEGFile());
OK。添加新类型的操作就此完成,在没有对原系统进行修改的基础上,只需加入简单的类型和操作即可完成原来看似复杂的操作,结果证明新架构经得起考验,爷爷也为小王竖起了大拇指。事实证明,只要有更合理的设计与架构,在基于面向对象和.NET框架的基础上,完全可以实现类似于插件的可扩展系统,并且无须编译即可更新扩展。
这一切是如何神奇般地实现了呢?回顾从设计到实现的各个环节,小王深知这都是源于多态机制的神奇力量,那么究竟什么是多态,.NET中如何来实现多态呢?
1.4.6 多态的类型、本质和规则
从小王一系列大刀阔斧的改革中,我们不难发现是多态、是面向对象技术成就了FileLoader的强大与灵活。回过头来,结合FileLoader系统的实现分析,我们也可以从技术的角度来进一步探讨关于多态的话题。
1.多态的分类
多态有多种分类的方式,Luca Cardelli在《On Understanding Types, Data Abstraction, and Polymorphism》中将多态分为四类:强制的、重载的、参数的和包含的。本节可以理解为包含的多态,从面向对象的角度来看,根据其实现的方式我们可以进一步分为基类继承式多态和接口实现式多态。
(1)基类继承式多态
基类继承多态的关键是继承体系的设计与实现,在FileLoader系统中File类作为所有资料类型的基类,然后根据需求进行逐层设计,我们从架构设计图中可以清楚地了解继承体系关系。在客户端调用时,多态是以这种方式体现的:
Files myFile = new WORDFile(); myFile.Open();
myFile是一个父类Files变量,保持了指向子类WORDFile实例的引用,然后调用一个虚方法Open,而具体的调用则决定于运行时而非编译时。从设计模式角度看,基类继承式多态体现了一种IS-A方式,例如WORDFile IS-A Files就体现在这种继承关系中。
(2)接口实现式多态
多态并非仅仅体现在基于基类继承的机制中,接口的应用同样能体现多态的特性。区别于基类的继承方式,这种多态通过实现接口的方法约定形成继承体系,具有更高的灵活性。从设计模式的角度来看,接口实现式多态体现了一种CAN-DO关系。同样,在万能加载器的客户端调用时,也可以是这样的实现方式:
IFileOpen myFile = new WORDFile(); myFile.Open();
当然,很多时候这两种方式都是混合应用的,就像本节的FileLoader系统的实现方式。
2.多态的运行机制
从技术实现角度来看,是.NET的动态绑定机制成就了面向对象的多态特性。那么什么是动态绑定,.NET又是如何实现动态绑定呢?这就是本节关于多态的运行机制所要探讨的问题。
动态绑定,又叫晚期绑定,是区别与静态绑定而言的。静态绑定在编译期就可以确定关联,一般是以方法重载来实现的;而动态绑定则在运行期通过检查虚拟方法表来确定动态关联覆写的方法,一般以继承和虚方法来实现。在.NET中,虚方法以virtual关键字来标记,在子类中覆写的虚方法则以override关键字标记。从设计角度考量,通常将子类中共有的但却容易变化的特征抽取为虚函数在父类中定义,而在子类中通过覆写来重新实现其操作。
注意
严格来讲,.NET中并不存在静态绑定。所有的.NET源文件都首先被编译为IL代码和元数据,在方法执行时,IL代码才被JIT编译器即时转换为本地CPU指令。JIT编译发生于运行时,因此也就不存在完全在编译期建立的关联关系,静态绑定的概念也就无从谈起。本文此处仅是参照C++等传统语言的绑定概念,读者应区别其本质。
关于.NET通过什么方式来实现虚函数的动态绑定机制,详细情况请参阅本章1.2节“什么是继承”的详细描述。在此,我们提取万能加载器FileLoader中的部分代码,来深入分析通过虚方法进行动态绑定的一般过程:
abstract class Files: IFileOpen { public abstract void Open(); public void Delete() { //实现对文件的删除处理 } } abstract class DocFile: Files { public int GetPageCount() { //计算文档页数 } } class WORDFile : DocFile { public override void Open() { Console.WriteLine("Open the WORD file."); } }
在继承体系的实现基础上,接着是客户端的实现部分:
Files myFile = new WORDFile(); myFile.Open();
针对上述示例,具体的调用过程,可以小结为:
编译器首先检查myFile的声明类型为Files,然后查看myFile调用方法是否被实现为虚方法。如果不是虚方法,则直接执行即可;如果是虚方法,则会检查实现类型WORDFile是否重写该方法Open,如果重写则调用WORDFile类中覆写的方法,例如本例中就将执行WORDFile类中覆写过的方法;如果没有重写,则向上递归遍历其父类,查找是否覆写该方法,直到找到第一个覆写方法调用才结束。
3.多态的规则和意义
·多态提供了对同一类对象的差异化处理方式,实现了对变化和共性的有效封装和继承,体现了“一个接口,多种方法”的思想,使方法抽象机制成为可能。
·在.NET中,默认情况下方法是非虚的,以C#为例必须显式地通过virtual或者abstract标记为虚方法或者抽象方法,以便在子类中覆写父类方法。
·在面向对象的基本要素中,多态和继承、多态和重载存在紧密的联系,正如前文所述多态的基础就是建立有效的继承体系,因此继承和重载是多态的实现基础。
1.4.7 结论
在爷爷大寿之际,小王终于完成了送给爷爷的生日礼物:万能加载器。看到爷爷轻松地玩着电脑,小王笑开了花,原来幸福是面向对象的。
在本节中,花了大量的笔墨来诠释设计架构和面向对象,或多或少有些喧宾夺主。然而,深入地了解多态及其应用,正是体现在设计模式、软件架构和面向对象的思想中;另一方面,也正是多态、继承和封装从技术角度成就了面向对象和设计模式,所以深入的理解多态就离不开大肆渲染以消化设计,这正是多态带来的艺术之美。