《你必须知道的.NET》读书实践:一个基于OO的万能加载器的实现
此篇已收录至《你必须知道的.Net》读书笔记目录贴,点击访问该目录可以获取更多内容。
一、关于万能加载器
简而言之,就是孝顺的小王想开发一个万能程序,可以一键式打开常见的计算机资料,例如:文档、图片和影音文件等,只需要安装一个程序就可以免去其他应用文件的管理(你让其他耗费了巨资打造的软件情何以堪...),于是就有了这个万能加载器(FileLoader)。
初步分析之后,小王总结了这个万能加载器的功能点如下:
(1)能够打开常见文档类资料:txt、word、pdf、visio等;
(2)能够打开常见图片类资料:jpg、gif、png等;
(3)能够打开常见音频类资料:avi、mp3等;
(4)支持简单可用的类型扩展接口,易于实现更多文件类型的加载;(这一条是重点,也是OO的魅力所在)
小王决定用这个软件作为胜利礼物送给爷爷,于是睡不着觉,非要起来装逼,绘制了一个基本的系统流程框架如下:
二、面向过程的实现
没有过多的思考,小王按照系统框架图,开始了最初的设计实现:
(1)第一步,设计了一个枚举,它展示了系统可支持的文件类型,以文件扩展名来划分:
public enum FileType { doc, //Word文档 pdf, //PDF文档 txt, //文本文档 ppt, //Ponwerpoint文档 jpg, //jpg格式图片 gif, //gif格式图片 mp3, //mp3音频文件 avi, //avi视频文件 all //所有类型文件 }
(2)第二步,有了支持的文件类型,接下来就得有一个文件类,来代表不同类型的文件资料:
public class Files { private FileType fileType; public FileType FileType { get { return this.fileType; } } }
(3)第三步,构建一个打开各种文件的管理类,封装了打开各种文件的具体打开方式:
public 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."); } }
这个长长的具体实现方法已经让小王写得蛋疼菊紧了,还有OpenAviFile、OpenGifFile等一大波的实现方式还没来得及写啊!
(4)第四步,为了能够出一个demo版本,小王放弃了继续写实现方式的过程,进入系统调用端的实现:
public class FileClient { public static void Main(string[] args) { Console.WriteLine("Welcome to use MyFileLoader!"); // 首先启动文件加载器 FileManager fm = new FileManager(); // 获取用户输入的文件类型 Console.WriteLine("Please enter the file type to open:"); string fileType = Console.ReadLine(); Files file = new Files() { FileType = GetFileType(fileType) }; switch (file.FileType) { case FileType.doc: fm.OpenDocFile(); break; case FileType.pdf: fm.OpenPdfFile(); break; case FileType.mp3: fm.OpenMp3File(); break; case FileType.jpg: fm.OpenJpgFile(); break; case FileType.txt: break; case FileType.ppt: break; case FileType.gif: break; case FileType.avi: break; case FileType.all: break; default: break; } Console.ReadKey(); } private static FileType GetFileType(string fileType) { FileType type; switch (fileType) { case "doc": type = FileType.doc; break; case "pdf": type = FileType.pdf; break; case "jpg": type = FileType.jpg; break; case "mp3": type = FileType.mp3; break; default: type = FileType.all; break; } return type; } }
小王虽然还有很多具体方式没有实现,但他还是兴奋地按下了F5调试了一把:
小王兴高采烈得把demo拿给爷爷看看,结果爷爷说:打开个rm格式的小电影让我瞧瞧,嘿嘿!But,小王的系统还没有支持这一格式,于是只好回去完善功能了。
But,小王发现自己的系统好像很难再插进一脚,除了添加新的文件支持类型,修改打开文件操作的代码,还得在FileManager类中添加新的支持代码,最后还要在客户调用端添加对应的打开调用操作,简直就是一场灾难,这个万能加载器该怎么应付下一次的需求变化呢,小王陷入了沉思。
经过一番分析,小王总结了当前设计的几个重要问题如下:
(1)需要深度地调整FileClient客户端,给系统维护带来了很大麻烦!(客户端应该保持相对的稳定)
(2)整个实现都是面向过程的方式,没有面向对象的影子!(Word、Pdf、Mp3都是可以实现的独立对象)
(3)没有实现可复用,其实OpenDocFile、OpenPdfFile等方法有很多可复用的代码。
(4)任何修改都会导致整个系统洗礼一次,无法轻松完成起码的系统变更和扩展!(这对当前系统来说将是致命的打击)
三、面向对象的实现
分析完最初的实现,小王经过了短期的郁闷和摸索,终于找到了阿里巴巴念动芝麻之门打开的魔咒,他想到了基于OO的多态来重构之前的设计,也长叹了一口气:去你*个大榴莲!看我用OO思想重写一遍!
(1)第一个重构:
将各个对象的属性和行为相分离,将打开文件这一行为封装为接口,再由其他类来实现这一接口,有利于系统的扩展同时还减少了类与类之间的依赖。
public interface IFileOpen { void Open(); }
(2)第二个重构:
首先是将Word、PDF、Txt等业务实体抽象为对象,并在每一个相应的对象内部来处理本对象类型的文件的打开工作,这样各个类型之间的交互操作就被分离出来,也很好的体现了单一职责的设计原则。
其次是将相似的类抽象出公共基类,在基类中实现具有共性的特征,并由子类继承父类的特征。这种设计体现了开放封闭的设计原则,如果有新的类型需要扩展,只需要选择继承合适的基类成员,实现新类型的特征代码即可。
接下来一个一个实现,首先是公共基类Files:
public abstract class Files : IFileOpen { private FileType fileType = FileType.doc; public FileType FileType { get { return this.fileType; } protected set { this.fileType = value; } } public abstract void Open(); }
其次是各类型文件的基类DocFile、ImageFile和MediaFile,分别代表文档类型、图片类型和媒体类型三个大类:
public abstract class DocFile : Files { public int GetPageCount() { // 计算文档页数 return 250; } } public abstract class ImageFile : Files { public void ZoomIn() { // 放大比例 } public void ZoomOut() { // 缩小比例 } } public abstract class MediaFile : Files { public void NextFrame() { // 下一帧 } public void PrevFrame() { // 上一帧 } }
最终终于到了实现具体文件类型的类定义的时候了,在此仅以Word和PDF类型为例来说明:
public class WordFile : DocFile { public WordFile() { FileType = FileType.doc; } public override void Open() { Console.WriteLine("Open the WORD file."); } } public class PDFFile : DocFile { public PDFFile() { FileType = FileType.pdf; } public override void Open() { Console.WriteLine("Open the PDF file."); } }
(3)第三个重构:提供一个资料管理的统一入口LoadManager来进行资料的统一管理。
public class LoadManager { private IList<Files> files = new List<Files>(); public IList<Files> Files { get { return this.files; } } // 加载指定文件到集合中 public void LoadFiles(Files file) { this.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) { // 根据指定文件路径返回文件类型 System.IO.FileInfo fi = new System.IO.FileInfo(fileName); return (FileType)Enum.Parse(typeof(FileType), fi.Extension.Substring(1))); } }
(4)第四个重构:调整FileClient客户端调用代码,实现根据所需文件进行加载。
public class FileClient { public static void Main(string[] args) { Console.WriteLine("Welcome to use MyFileLoader!"); // 首先启动文件加载器 LoadManager lm = new LoadManager(); // 添加需要处理的文件 lm.LoadFiles(new WordFile()); lm.LoadFiles(new PDFFile()); lm.LoadFiles(new JPGFile()); lm.LoadFiles(new AVIFile()); // 获取爷爷选择文件类型 Console.WriteLine("Please enter the file type to open:"); string fileName = Console.ReadLine(); FileType type = lm.GetFileType(fileName); // 打开爷爷选择的文件 foreach (MyFileLoader.OOLoader.Files file in lm.Files) { if(file.FileType.Equals(type)) { lm.OpenFile(file); } } Console.ReadKey(); } }
这次,小王自信满满地按下了F5,基本的文件处理已经不在话下了:
而且,更为重要的是,当爷爷需要看不同格式的小电影格式时,小王只需要做简单的调整即可满足需求的变化。例如,假如需要观看MPEG格式的文件,只需要增加处理MPEG文件的类型MPEGFile,使其继承自MediaFile,实现具体的Open方法即可:
public class MPEGFile : MediaFile { public MPEGFile() { FileType = FileType.mpeg; } public override void Open() { Console.WriteLine("Open the MPEG file."); } }
但是,如果要正常使用,还得再客户端增加对其的处理定义:
// 添加需要处理的文件 ...... lm.LoadFiles(new MPEGFile());
现在再来看看能否打开mpeg文件了:
自此,爷爷再也不用担心小王的节操了,小王也可以洗洗睡了。
在小王睡觉之前,重新梳理了一下现在的设计结构图:
四、借助反射的重构
小王睡了几个安稳觉,某一晚又被爷爷的新需求叫醒,又是一次修改客户端代码的操作,原来之前的设计还是需要改客户端。于是,小王冥思苦想,决定使用配置文件和反射动态获取来重构,避免在客户端出现耦合(客户端和具体的文件类型)。
(1)第一个重构:添加一个用于定义文件加载类型的配置文件;
<?xml version="1.0" encoding="utf-8" ?> <objects> <object name="WordFile" namespace="MyFileLoader.Model" /> <object name="PDFFile" namespace="MyFileLoader.Model" /> <object name="JPGFile" namespace="MyFileLoader.Model" /> <object name="AVIFile" namespace="MyFileLoader.Model" /> <object name="MPEGFile" namespace="MyFileLoader.Model" /> <!-- 要添加加载的文件类型在这里添加即可无痛扩展 --> </objects>
以后有新增的需求,编写好新的文件实现类后直接在配置文件里边扩展就行了,不再动一点的客户端调用的代码。
(2)第二个重构:添加一个MyXmlFactory解析配置文件并通过反射动态地获取已定义的加载文件类型;
public class MyXmlFactory { public IDictionary<string, Files> definedFiles = new Dictionary<string, Files>(); public MyXmlFactory(string configPath) { this.InitializeFileTypes(configPath); } private void InitializeFileTypes(string configPath) { XElement root = XElement.Load(configPath); foreach (var item in root.Elements("object")) { // 通过反射动态创建具体类型实例 string className = item.FirstAttribute.Value; string classPath = item.LastAttribute.Value; Files file = (Files)System.Reflection.Assembly.Load(classPath).CreateInstance(classPath + "." + className); definedFiles.Add(new KeyValuePair<string, Files>( className, file )); } } }
通过反射在运行期动态获取,以避免耦合在客户端。需要注意的是,这里我是建立的控制台程序,因此需要将所有的业务实体对象封装到单独的dll(可以新建一个类库项目)中,否则无法通过Assembly.Load出来。
(3)第三个重构:修改FileClient代码,至此更新不再更改客户端代码;
// 添加需要处理的文件 //lm.LoadFiles(new WordFile()); //lm.LoadFiles(new PDFFile()); //lm.LoadFiles(new JPGFile()); //lm.LoadFiles(new AVIFile()); //lm.LoadFiles(new MPEGFile()); MyXmlFactory xmlFactory = new MyXmlFactory("DefinedTypes.config"); if(xmlFactory.definedFiles.Count > 0) { foreach (var definedType in xmlFactory.definedFiles) { lm.LoadFiles(definedType.Value); } }
这里只需将原来单独的添加文件的方法改为调用MyXMLFactory的反射方法即可。小王重构之后,长叹了一口气,这下可以休息一下了,第二天爷爷很满意这个版本,给了小王一个奖品:
总结:后续设计之路还很漫长,但事实证明:只要有更合理的设计和架构,在基于OO和.NET框架的基础之上,完全可以实现类似于插件式的可扩展系统,并且无需编译即可更新扩展。
参考资料
本文源自王涛(anytao)的《你必须知道的.NET(第二版)》,剧情加以YY写成,感谢金馆长熊猫表情。