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

Posted IDEA

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在不修改代码的情况下无限扩展应用项目相关的知识,希望对你有一定的参考价值。

在许多需要分模块开发,较为复杂的应用项目(如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.     /// <summary>  
  13.     /// 足球  
  14.     /// </summary>  
  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. /// <summary>  
  2. /// 排球  
  3. /// </summary>  
  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<Lazy<CommonLib.IBall, CommonLib.IMetaData>> 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<Lazy<CommonLib.IBall, CommonLib.IMetaData>> mBalls;  

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

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

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.     /// <summary>  
  12.     /// 篮球  
  13.     /// </summary>  
  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)强大吧?

以上是关于在不修改代码的情况下无限扩展应用项目的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C++ 中使用 Detours 扩展程序内函数而不进入无限循环?

.net ORM,它允许我在不修改代码的情况下扩展我的代码(开放封闭原则)

星际无限:Filecoin与Dapper Labs达成合作

OkHttp 和 Retrofit 无限 Post 请求在后台

Unity - 如何在无限循环的情况下停止播放模式?

iOS 应用程序如何在后台无限期地保持 TCP 连接处于活动状态?