此篇已收录至《你必须知道的.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 ;
}
} 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 > )
{
foreach (var definedType in xmlFactory.definedFiles)
{
lm.LoadFiles(definedType.Value);
}
}

  这里只需将原来单独的添加文件的方法改为调用MyXMLFactory的反射方法即可。小王重构之后,长叹了一口气,这下可以休息一下了,第二天爷爷很满意这个版本,给了小王一个奖品:

总结:后续设计之路还很漫长,但事实证明:只要有更合理的设计和架构,在基于OO和.NET框架的基础之上,完全可以实现类似于插件式的可扩展系统,并且无需编译即可更新扩展

参考资料

本文源自王涛(anytao)的《你必须知道的.NET(第二版)》,剧情加以YY写成,感谢金馆长熊猫表情。

作者:周旭龙

出处:http://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

《你必须知道的.NET》读书实践:一个基于OO的万能加载器的实现的更多相关文章

  1. 一个简单的AMD模块加载器

    一个简单的AMD模块加载器 参考 https://github.com/JsAaron/NodeJs-Demo/tree/master/require PS Aaron大大的比我的完整 PS 这不是一 ...

  2. 实现一个类 RequireJS 的模块加载器 (二)

    2017 新年好 ! 新年第一天对我来说真是悲伤 ,早上兴冲冲地爬起来背着书包跑去实验室,结果今天大家都休息 .回宿舍的时候发现书包湿了,原来盒子装的牛奶盖子松了,泼了一书包,电脑风扇口和USB口都进 ...

  3. 系统管理员必须知道的PHP安全实践

    Apache web 服务器提供了这种便利 :通过 HTTP 或 HTTPS 协议,访问文件和内容.配置不当的服务器端脚本语言会带来各种各样的问题.所以,使用 PHP 时要小心.以下是 25 个 PH ...

  4. 「从零单排HBase 06」你必须知道的HBase最佳实践

    前面,我们已经打下了很多关于HBase的理论基础,今天,我们主要聊聊在实际开发使用HBase中,需要关注的一些最佳实践经验. 1.Schema设计七大原则 1)每个region的大小应该控制在10G到 ...

  5. 你必须知道的.net读书笔记之第二回深入浅出关键字---对抽象编程:接口和抽象类

    请记住,面向对象思想的一个最重要的原则就是:面向接口编程. 借助接口和抽象类,23个设计模式中的很多思想被巧妙的实现了,我认为其精髓简单说来就是:面向抽象编程. 抽象类应主要用于关系密切的对象,而接口 ...

  6. 你必须知道的.net读书笔记第四回:后来居上:class和struct

     基本概念 1.1. 什么是class? class(类)是面向对象编程的基本概念,是一种自定义数据结构类型,通常包含字段.属性.方法.属性.构造函数.索引器.操作符等.因为是基本的概念,所以不必在此 ...

  7. C#刨根究底:《你必须知道的.NET》读书笔记系列

    一.此书到底何方神圣? <你必须知道的.NET>来自于微软MVP—王涛(网名:AnyTao,博客园大牛之一,其博客地址为:http://anytao.cnblogs.com/)的最新技术心 ...

  8. 【模块化编程】理解requireJS-实现一个简单的模块加载器

    在前文中我们不止一次强调过模块化编程的重要性,以及其可以解决的问题: ① 解决单文件变量命名冲突问题 ② 解决前端多人协作问题 ③ 解决文件依赖问题 ④ 按需加载(这个说法其实很假了) ⑤ ..... ...

  9. 《你必须知道的.NET》读书笔记一:小OO有大智慧

    此篇已收录至<你必须知道的.Net>读书笔记目录贴,点击访问该目录可以获取更多内容. 一.对象  (1)出生:系统首先会在内存中分配一定的存储空间,然后初始化其附加成员,调用构造函数执行初 ...

随机推荐

  1. SpringBoot-目录及说明

    今天开始抽时间整理SpringBoot的内容这里可以作为一个目录及说明相关的资料都可以跳转使用 说明: 目录: 一:创建SpringBoot项目 1)Maven创建 (1)使用命令行创建Maven工程 ...

  2. java PDF分页打印

    将获取的pdf文件按页拆分:参考https://q.cnblogs.com/q/99944/ pdf文件有多页,第一页需设置横向打印,其他页设置为纵向打印. PDDocument document = ...

  3. notes for python简明学习教程(1)

    print总是以(\n)作为结尾,不换行可以指定一个空 end='' 字符串前面+r, 原始字符串 \ 显示行连接 input()函数以字符串的形式 返回键入的内容 函数参数, 有默认值的形参要放在形 ...

  4. vue基础5-生命周期

    1.vue实例的生命周期  1.1.什么是生命周期? --从Vue实例创建.运行.销毁期间,总是伴随着各式各样的事件,这些事件,统称为生命周期!  1.2.生命周期钩子:就是生命周期事件的别名而已:  ...

  5. 2019 蓝桥杯省赛 A 组模拟赛(一)-修建公路

    题目: 蒜头国有 nn 座城市,编号分别为 0,1,2,3,...,n-1.编号为 x 和 y 的两座城市之间如果要修高速公路,必须花费 x|y 个金币,其中|表示二进制按位或. 吝啬的国王想要花最少 ...

  6. jquery 操作服务端控件,select 控件

    <asp:DropDownList ID="ddl" runat="server"></asp:DropDownList> <se ...

  7. jmeter数据库,charles抓包,Python循环语句

    jmeter数据库,charles抓包,Python循环语句 一.Jemeter数据库 添加jar包数据库 jemeter=>浏览 添加JDBC Connection Configuration ...

  8. VS 2013+ ArcGIS 10.3 AddIn 断点不断异常解决

    1. http://resources.arcgis.com/en/help/arcobjects-net/conceptualhelp/index.html#/How_to_debug_add_in ...

  9. vs2017 调试时 浏览器关闭不想中断调试

    解决方案 工具—>选项—>项目和解决方案—>web项目-->去点“浏览器窗口关闭时停止调试”前面的勾去掉>>>

  10. 【逆元】HDU-1576

    逆元: 同余方程 ax≡1(mod n),gcd(a,n) = 1 时有解,这时称求出的 x 为 a 的对模n的乘法逆元.(注意:如果gcd(a,n)如果不等于1则无解),解法还是利用扩展欧几里得算法 ...