热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

在不修改代码的情况下无限扩展应用项目

在许多需要分模块开发,较为复杂的应用项目(如ERP之类)中,如何做到轻松扩展,往往是一个头疼的问题。在传统条件下,我们会把各个功能分布在不同的类库中,每添加一个功能就引用一个程序集,而这种方法,我

在许多需要分模块开发,较为复杂的应用项目(如ERP之类)中,如何做到轻松扩展,往往是一个头疼的问题。

在传统条件下,我们会把各个功能分布在不同的类库中,每添加一个功能就引用一个程序集,而这种方法,我们会发现,当你每添加一个新扩展后,都要对新增的程序集进行引用,这样也意味着,你每次都要重新编译一次主应用程序,这一来一往,维护成本也是有的。

到了.NET 3.5时代,你可能会想到Addin,但这个方法也会带来扩展成本。

而我们所追求的最完美解决方案是:

如果我od 们编写完应用程序后,可以在原有程序不变的情况下,无限添加扩展就好了。也就是说,我把应用A在用户的机器上安装好了,后来我做了一点扩展,这个新功能我已经编译到fc.dll类库中了,可我不想每次升级都要把EXE文件和所有组件重新编译,我只需要把新的fc.dll复制到应用安装目录下就可以了。

 

也许这样一来,维护成本可以大大降低了,到了.NET 4.0时代,利用MEF框架确实可以做到以上要求,但前提条件是:

1、在开发项目前,对整个项目的结构和规范必须明确,至少整个应用程序是什么样子的,你必须在大脑里面有个底。

2、编写一个公共类库,里面包含所有将来要进行扩展组件的行为规范,也就是在这个公共类库中定义所有将来可能被实现的接口,后面所有扩展的程序集都必须实现这些接口。

 

本文涉及的内容可能有些深奥,但愿大家可以接受,接受不了也没关系,毕竟许多东西是需要时间去掌握的。

 

我弄个简单的例子吧,比如,我现在要开发一个应用,在一个窗口上点击按钮后,显示对应球类的名字,如“足球”、“皮球”、“排球”等。但是,可能当初我只给出两个选项——足球和排球,这是我在把程序给客户前就扩展的两个程序集,但过了几天,我突然想起,还有“羽毛球”、“篮球”等,于是,我要为应用程序再加两个dll,但我希望应用程序扩展后不用修改代码,无论我将来扩展100个还是10000个dll我都不需要重新生成主程序,我只要把新的dll扔到应用程序中的Ext文件夹中就可以了。

 

我们来看看如何实现。

1、新建一个公共类库,写两个接口,IBall接口就是与球类信息有关的类,提供扩展时实现该接口。

[csharp]  view plain  copy
 
  1. public interface IBall  
  2. {  
  3.     string GetInformation();  
  4. }  


它有一个公共方法GetInformation,返回对应球类的名字,如“足球”.

另一个接口是用来描述元数据的。

[csharp]  view plain  copy
 
  1. public interface IMetaData  
  2. {  
  3.     string BallType { get; }  
  4. }  

为什么要定义这个元数据的接口呢?就是为了识别我们应用程序调用了哪个扩展。

比如,FootBall(足球)类扩展实现了IBall接口,VolleyBall(排球)类扩展也实现了IBall接口,BasketBall(篮球)类扩展也实现了IBall接口,可能以后会更多,所有的扩展都实现IBall,那么,我们怎么知道我们正在调用的足球?而不是篮球呢?所以,就需要这个IMetaData类,在进行扩展的导出类时,我们为每一个类型定义一下IMetaData的BallType属性,例如,我在定义足球类时,我定义BallType为“foot ball”,在定义排球类时,把BallType设置为“volley ball”,这样,在我们的应用程序中,就可以通过这个来判断我们正在调用哪个扩展,当然,如果你不需要明确知道调用哪个扩展,这个元数据就可以忽略。

 

2、分别编写两个类库,符合以下两个条件:

a、实现IBall接口。

b、用ExportAttribute标识为导出类型。

MEF的类都是来自System.ComponentModel.Composition程序集,在需要的地方引用就行了,如何引用程序集,我就不说了,这是基础知识。这些类分布在以下三个命名空间。

System.ComponentModel.Composition

System.ComponentModel.Composition.Hosting

System.ComponentModel.Composition.Primitives

 

[csharp]  view plain  copy
 
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Text;  
  5. using System.Threading.Tasks;  
  6. using System.ComponentModel.Composition;  
  7. using System.ComponentModel.Composition.Hosting;  
  8.   
  9. namespace BallLibA  
  10. {  
  11.   
  12.     ///   
  13.     /// 足球  
  14.     ///   
  15.     [Export(typeof(CommonLib.IBall))]  
  16.     [ExportMetadata("BallType","Foot Ball")]  
  17.     public class FootBall:CommonLib.IBall  
  18.     {  
  19.         public string GetInformation()  
  20.         {  
  21.             return "足球";  
  22.         }  
  23.     }  
  24.   
  25.   
  26. }  


在MEF中,我们不需要实现提供元数据的接口,只需要在ExportMetadata特性中直接为属性设置值就行,运行时会自动生成实现元数据(本例是IMetaData接口)的类。

 

接照同样的方法,我们再做一个类库。

[csharp]  view plain  copy
 
  1. ///   
  2. /// 排球  
  3. ///   
  4. [Export(typeof(CommonLib.IBall))]  
  5. [ExportMetadata("BallType", "Volley Ball")]  
  6. public class VolleyBall : CommonLib.IBall  
  7. {  
  8.     public string GetInformation()  
  9.     {  
  10.         return "排球";  
  11.     }  
  12. }  

现在,我们的项目已经有两个扩展了。

 

3、我们来实现我们的主应用程序,我们只需引用我们前面编写的公共类库中的接口即可,而扩展的dll我们不需要引用,MEF会自动寻找。因此,把所有扩展的程序集都生成dll文件,然后统一扔到与exe文件同一位置的Ext文件夹中就行了,你有1000个dll就全部扔到文件夹里就行,MEF会自动寻找。

我们用一个WinForm程序作为主程序,如下图所示。



在程序运行时,会根据Ext目录下的所有扩展的dll自动发现所有程序集,然后显示在ComboBox中,我们选择对应的球类,然后点击按钮,这样在文本框中就会对应地显示球类的名称。

[csharp]  view plain  copy
 
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.ComponentModel;  
  4. using System.Data;  
  5. using System.Drawing;  
  6. using System.Linq;  
  7. using System.Text;  
  8. using System.Threading.Tasks;  
  9. using System.Windows.Forms;  
  10. using System.ComponentModel.Composition;  
  11. using System.ComponentModel.Composition.Hosting;  
  12.   
  13. namespace TestApp  
  14. {  
  15.     public partial class Form1 : Form  
  16.     {  
  17.         CompositionContainer myContainer = null;  
  18.         // 引入的组合类型  
  19.         [ImportMany]  
  20.         IEnumerable> mBalls;  
  21.         public Form1()  
  22.         {  
  23.             InitializeComponent();  
  24.             DirectoryCatalog catl = new DirectoryCatalog("Ext");  
  25.             myContainer = new CompositionContainer(catl);  
  26.             try  
  27.             {  
  28.                 myContainer.ComposeParts(this);//组合组件  
  29.             }  
  30.             catch (Exception ex)  
  31.             {  
  32.                 MessageBox.Show(ex.Message);  
  33.             }  
  34.             var resBalls = (from t in mBalls  
  35.                             select t.Metadata.BallType).ToArray();  
  36.             this.comboBox1.DataSource = resBalls;  
  37.   
  38.         }  
  39.   
  40.         private void button1_Click(object sender, EventArgs e)  
  41.         {  
  42.             if (this.comboBox1.SelectedIndex == -1)  
  43.             {  
  44.                 MessageBox.Show("请选择一个扩展。"); return;  
  45.             }  
  46.             string ballName = this.comboBox1.SelectedItem.ToString();  
  47.             // 取出要执行哪个扩展程序集  
  48.             var ballInstance = mBalls.FirstOrDefault(x => x.Metadata.BallType == ballName);  
  49.             if (ballInstance != null)  
  50.             {  
  51.                 this.txtResult.Text = ballInstance.Value.GetInformation();  
  52.             }  
  53.         }  
  54.     }  
  55. }  


从上面的代码中,可以总结出MEF的用法,这方法你有兴趣的话可以背下来,因为无论你用到什么项目,思路都是一样的。

1、声明一个CompositionContainer变量是必须的,因为它可以用来指示当前应用程序与哪些扩展程序集进行合并。

2、在实例化CompositionContainer时,我使用DirectoryCatalog类,为什么?因为这个类好用,你只需要告诉它你扩展的dll放在哪个文件夹就行了。它会在你指定的文件夹里面自动找到导出的扩展类。

3、有导出类,自然就有导入类,因为我们的所有扩展都是实现IBall接口的,所以,扩展的类的导出类型应使用IBall,这样,凡是声明为导出类的都会被MEF发现并自动加载。

所以,导出是针对扩展的程序集而言的,那导入就好理解了,就是针对我们的主应用程序而言,像本例,WinForm应用作为主程序,所有扩展都是在这个WinForm中使用的,所以这个WinForm就必须对类型进行导入。因此才有了以下代码。

[csharp]  view plain  copy
 
  1. // 引入的组合类型  
  2. [ImportMany]  
  3. IEnumerable> mBalls;  

使用Lazy来延迟实例化的好处是提高性能,记住,加了Import的导入类型是不用new的,因为DirectoryCatalog在Ext文件夹下找到所有的dll都会自动实例化,这就是要用延迟实便化的原因,只有在用的时候才new,不然,如果我的扩展目录下有100000个dll,350000000个类,那你一运行就全部实例化,那这性能估计要把内存用爆。

前面我说过,IMetaData用于标识元数据,我们不必自己去实现,而我们也不必指事实上哪个接口,因为上面代码中,Lazy就有两个泛型参数,看到没?

T是我们要导入的类型,本例中是IBall,注意,我们这里的类型一定要是公共的接口,不是扩展的具体类,不然就实现不了无限扩展的目的,接口用途就是它有通用性。

TMetadata就是用来标识元数的类型,本例是IMetaData接口,所以,前面我为什么不用指定IMetaData的原因,因为这里会指定,MEF会沿着这个线索自动搜索它的属性BallType。

 

在实例化CompositionContainer容器后,要记得调用扩展方法ComposeParts,告诉MEF,所有扩展的程序集将和当前实例进行组合,不然你将无法调用。

 

现在,你运行一个这个WinForm,你就明白了。

 

看到了吧,FootBall和VolleyBall类所在的两个程序集我并没有在项目中,引用,只是把它们扔到Ext目录下,应用程序就自动识别了。

我们的WinForm程序不用修改一行代码。

 

如果你还不信的话,我们接下来再增加一个dll,定义一个BasketyBall(篮球类),然后,把这个篮球类库也生成一个dll,同样扔到Ext目录下,而WinForm程序我根本不需要改动。

[csharp]  view plain  copy
 
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Text;  
  5. using System.Threading.Tasks;  
  6. using System.ComponentModel.Composition;  
  7. using System.ComponentModel.Composition.Hosting;  
  8.   
  9. namespace BallLibC  
  10. {  
  11.     ///   
  12.     /// 篮球  
  13.     ///   
  14.     [Export(typeof(CommonLib.IBall))]  
  15.     [ExportMetadata("BallType","Basket Ball")]  
  16.     public class BasketBall:CommonLib.IBall  
  17.     {  
  18.         public string GetInformation()  
  19.         {  
  20.             return "篮球";  
  21.         }  
  22.     }  
  23. }  

同样道理,把这个类库编译成dll,然后扔到Ext文件下,然后你再运行一下WinForm程序看看。

看到了吧,我没有对WinForm做任何修改,只是在Ext目录下多放了一个dll而已,运行后,程序就自动识别并找到对应的类型了。下拉列表框中就自动多了一个Basket Ball的选项了。选择它,并单击按钮,这个BadketBall类就被执行了,输出“篮球”。

 

以此类推,你再添加一千个一万个dll,只要它符合IBall接口规范并设置导出,然后把这一千个一万个dll全放到Ext目录下,应用程序不需要做任何修改,运行后就会自动找到一千个一万个扩展类了。

这样一来,是不是节约了不少维护和升级成本了?MEF(Managed Extensibility Framework)强大吧?


推荐阅读
  • 基于Net Core 3.0与Web API的前后端分离开发:Vue.js在前端的应用
    本文介绍了如何使用Net Core 3.0和Web API进行前后端分离开发,并重点探讨了Vue.js在前端的应用。后端采用MySQL数据库和EF Core框架进行数据操作,开发环境为Windows 10和Visual Studio 2019,MySQL服务器版本为8.0.16。文章详细描述了API项目的创建过程、启动步骤以及必要的插件安装,为开发者提供了一套完整的开发指南。 ... [详细]
  • 本文介绍了如何使用Aspose库将Office文件(如Word、PowerPoint)转换为HTML文件,并详细说明了在转换过程中可能出现的乱码问题及其解决方案。 ... [详细]
  • vue引入echarts地图的四种方式
    一、vue中引入echart1、安装echarts:npminstallecharts--save2、在main.js文件中引入echarts实例:  Vue.prototype.$echartsecharts3、在需要用到echart图形的vue文件中引入:   importechartsfrom"echarts";4、如果用到map(地图),还 ... [详细]
  • WPF项目学习.一
    WPF项目搭建版权声明:本文为博主初学经验,未经博主允许不得转载。一、前言记录在学习与制作WPF过程中遇到的解决方案。使用MVVM的优点是数据和视图分离,双向绑定,低耦合,可重用行 ... [详细]
  • C#实现文件的压缩与解压
    2019独角兽企业重金招聘Python工程师标准一、准备工作1、下载ICSharpCode.SharpZipLib.dll文件2、项目中引用这个dll二、文件压缩与解压共用类 ... [详细]
  • Hadoop的文件操作位于包org.apache.hadoop.fs里面,能够进行新建、删除、修改等操作。比较重要的几个类:(1)Configurati ... [详细]
  • 通过将常用的外部命令集成到VSCode中,可以提高开发效率。本文介绍如何在VSCode中配置和使用自定义的外部命令,从而简化命令执行过程。 ... [详细]
  • 零拷贝技术是提高I/O性能的重要手段,常用于Java NIO、Netty、Kafka等框架中。本文将详细解析零拷贝技术的原理及其应用。 ... [详细]
  • [转]doc,ppt,xls文件格式转PDF格式http:blog.csdn.netlee353086articledetails7920355确实好用。需要注意的是#import ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 开机自启动的几种方式
    0x01快速自启动目录快速启动目录自启动方式源于Windows中的一个目录,这个目录一般叫启动或者Startup。位于该目录下的PE文件会在开机后进行自启动 ... [详细]
  • 在软件开发过程中,经常需要将多个项目或模块进行集成和调试,尤其是当项目依赖于第三方开源库(如Cordova、CocoaPods)时。本文介绍了如何在Xcode中高效地进行多项目联合调试,分享了一些实用的技巧和最佳实践,帮助开发者解决常见的调试难题,提高开发效率。 ... [详细]
  • 本文详细解析了使用C++实现的键盘输入记录程序的源代码,该程序在Windows应用程序开发中具有很高的实用价值。键盘记录功能不仅在远程控制软件中广泛应用,还为开发者提供了强大的调试和监控工具。通过具体实例,本文深入探讨了C++键盘记录程序的设计与实现,适合需要相关技术的开发者参考。 ... [详细]
  • iOS snow animation
    CTSnowAnimationView.hCTMyCtripCreatedbyalexon1614.Copyright©2016年ctrip.Allrightsreserved.# ... [详细]
  • 本文详细介绍了 HTML 中 a 标签的 href 属性的多种用法,包括实现超链接、锚点以及调用 JavaScript 方法。通过具体的示例和解释,帮助开发者更好地理解和应用这些技术。 ... [详细]
author-avatar
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有