此篇已收录至《你必须知道的.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. mac中如何卸载pkg包

    ---恢复内容开始--- 参考地址.https://blog.csdn.net/play_fun_tech/article/details/27964861?utm_source=tuicool&am ...

  2. HashCode总结

    不同的实例对象的hashCode是不相同的 package com.cici.test;class DoubleLinkNode{ public int iData; public double dD ...

  3. ./graldew bash: ./gradlew: No such file or directory

    使用gradlew的项目,可以使用./gradlew assembelDebug 使用本地gradle编译的项目,并且配置了环境变量,可以使用gradle assembleDebug直接编译包

  4. 我的 FPGA 学习历程(08)—— 实验:点亮单个数码管

    数码管是一种常见的用于显示的电子器件,根据数码管大致可以分为共阴极和共阳极两种,下图所示的是一个共阳极的数码管的电路图(摘自金沙滩工作室的 51 开发板电路图),我的 AX301 开发板与这张图的情况 ...

  5. 一步一步 copy163: 网易严选 ---- vue-cli

    面试点 组件间通信 生命周期函数 路由 - 参数 - 重定向 vuex 参考 网易严选商城小程序全栈开发,域名备案中近期上线(mpvue+koa2+mysql) 小程序服务端源码地址 小程序源码地址 ...

  6. Idea集成maven插件

    学习目标 1.正确在idea上安装maven 2.安装后使用的基本操作 3.回顾安装步骤 安装过程 设置安装后自动下载功能 maven一键构建概念 我们的项目,往往都要经历编译. 测试. 运行. 打包 ...

  7. Deployment Characteristics of "The Edge" in Mobile Edge Computing

    移动边缘计算中的“边缘”部署特性 本文为SIGCOMM 2018 Workshop (Mobile Edge Communications, MECOMM)论文. 本文翻译了论文的关键内容. 摘要 移 ...

  8. 解决 spring-cloud-starter-zipkin 启动错误

    应用场景:Spring Boot 服务添加 Zipkin 依赖,进行服务调用的数据采集,然后进行 Zipkin-Server 服务调用追踪显示. 示例pom.xml配置: <parent> ...

  9. Mycat适合场景及不适合场景

    1.非分片字段查询 Mycat中的路由结果是通过分片字段和分片方法来确定的.例如下图中的一个Mycat分库方案: 根据 tt_waybill 表的 id 字段来进行分片 分片方法为 id 值取 3 的 ...

  10. [Swift]LeetCode162. 寻找峰值 | Find Peak Element

    A peak element is an element that is greater than its neighbors. Given an input array nums, where nu ...