.NET插件系统(三) 插件间通信问题——设计可自组织和注入的组装程序

一.  问题的背景

       动态系统的要求之一,是不同模块可以根据自身需求自动组装,这往往通过配置文件或用户选择进行。  这个基本问题在前面的文章中已经讲述过了。

       但新的问题来了,我们定义了不同的插件A,B,C,那么,不同插件之间的通信如何进行?

   如果系统本身的框架非常明晰而且不易更改,那么面向固定接口的方法是最简单方便的。 这也是大部分插件系统在“主结构”上使用的做法。

   但是,如果系统框架本身非常易变,连他们之间交互的接口都会随着问题的不同而不同。这就好像,系统中包含不同种类的插座和插头,我们需要自动将符合要求的插座和插头安装好,实现自动组网。如何实现这种自组织的组装程序呢?

       

二 . 具体的案例

      为了便于更好的说明问题,以一个我实际面对的设计问题进行分析,实在抱歉由于时间所限不能提供Demo.

      我需要开发一个数据挖掘和处理平台,不同的功能通过插件的形式接入系统,并形成如左边的可执行列表:

     

      用户可以很简单的通过拖拽,将左边的处理拖到后边的算法执行列表中,当点击"运行"按钮时,列表中的算法模块会被顺序或并行执行。

      这些算法是多样的,比如数据挖掘中常用的  数据筛选,分词,聚类,数据分类显示等功能。你可以不必了解这些算法本身是什么,但本文中,您可能需要了解他们之间的结果可能相互依赖。这些算法的共同特征,是必须依赖于前一个或多个算法的结果,同时本身还可以输出给其他算法。 例如,数据分类显示必须依赖于聚类的结果,聚类则必须依赖于前期分词和数据筛选的功能。

      这种结构很像顺序流动的数据流,或者像电网或自来水网的结构。那么问题来了,系统执行前不知道这些算法到底是什么,那怎么能提供插件间交互的需求?  一种做法是,读写数据库,只要上一个算法告诉下一个算法数据的位置在哪里就可以了,但这种做法很不“环保”,试想,好好的存在内存中的数据,干嘛要写到硬盘中再读出来呢?这会造成无谓的开销。

      另外,算法执行列表(右边)的顺序应该与组装顺序无关,意思是处在数据流上游的模块不一定就在执行列表的上游。

    我们必须设计一套方法,能实现这些算法的相互通信。

    

 三 . 声明可提供接口和注入接口需求

       首先,为了保证重用,算法模块之间的通信方式只能是接口或抽象类。不论如何,算法应该告诉管理器,它必须依赖什么,它可以提供什么。

       如果一个算法模块可以提供某接口的结果,那么它必须实现该接口。

   如果算法必须依赖某接口,那么它应该最少包含一个该接口的内部成员,或者,也实现之(本文没有考虑这种情况)。

       下面我们简单实现两个类:

        计算方法A可以输出接口B和C,但计算方法B必须得到两个接口B和C的结果。

[SelfBuildClassAttribute(new string[] { }, new string[] { "IB", "IC" })]
    [XFrmWorkAttribute("计算方法A", "IDataProcess", "可输出接口B和C", "123")]
    public class Test1 : AbstractProcessMethod, IC, IB
    {
        public string outputC
        {
            get;
            set;
        }

        public string outputB
        {
            get;
            set;
        }
        public override bool DataProcess()
        {
            outputC = "已经正确赋值C";
            outputB = "已经正确赋值B";
            return true;
        }

    }

    [SelfBuildClassAttribute(new string[] { "IB", "IC" }, new string[] { })]
    [XFrmWorkAttribute("计算方法B", "IDataProcess", "必须通过外界提供B和C的接口", "123")]
    public class Test2 : AbstractProcessMethod
    {
        [SelfBuildMemberAttribute("IB")]
        public IB calledIB { get; set; }

        [SelfBuildMemberAttribute("IC")]
        public IC callIC { get; set; }
        public override bool DataProcess()
        {

            XLogSys.Print.Debug(calledIB.outputB);
            XLogSys.Print.Debug(callIC.outputC);
            return true;
        }
    }

    两个方法方法非常简单,继承于AbstractProcessMethod类,你不需要关心这个类的具体内容,只需注意 Test1实现了两个接口IB和IC,这两个接口都能提供两个字符串。Test2类则必须获得IB和IC两个接口的字符串成员。

       我们可以通过自定义Attribute实现可提供和依赖的接口的标识。 本系统中使用了两个自定义的attribute:

  • [SelfBuildClassAttribute(new string[] { }, new string[] { "IB", "IC" })]
    两个必选形参,即需求的接口字符串列表 和 提供的接口字符串列表。
  • [SelfBuildMemberAttribute("IC")]
    要求被注入的需求者成员变量标识

        (XFrmWorkAttribute是插件的标记,详情可见我上一篇关于插件的文章)     

/// <summary>
/// 实现自组织算法的特性,它一般标记在模块的类名之前
/// </summary>
   public class SelfBuildClassAttribute:Attribute
    {
       /// <summary>
/// 要求的依赖项接口
/// </summary>
       public ICollection<string> dependInterfaceCollection
        {
            get;
            set;
        }
       /// <summary>
/// 可以输出的接口
/// </summary>
       public ICollection<string> outputInterfaceCollection
       {
           get;
           set;
       }
       public SelfBuildClassAttribute(string[] dependInterfaceName, string[] outputInterfaceName)
       {
           dependInterfaceCollection = new List<string>();
           outputInterfaceCollection = new List<string>();
           foreach (string rc in dependInterfaceName)
           {
               dependInterfaceCollection.Add(rc);
           }
           foreach (string rc in outputInterfaceName)
           {
               outputInterfaceCollection.Add(rc);
           }
       }
       public SelfBuildClassAttribute(ICollection<string> dependInterface, ICollection<string> outputInterfaceName)
       {
           dependInterfaceCollection = dependInterface;
           outputInterfaceCollection = outputInterfaceName;
       }
    }

    /// <summary>
/// 自组织成员特性,一般放置在类的  要求注入的成员名上
/// </summary>
    public  class SelfBuildMemberAttribute:Attribute
    {
        public string invokeName{get;set;}  //需要被注入的依赖项接口
        public SelfBuildMemberAttribute(string myInvokeName)
        {
            invokeName = myInvokeName;
        }
    }

 这两个类的作用已经在注释上写清楚了,您可以结合Test1和Test2两个类的具体实现来理解:  Test1不需要依赖任何接口,但可以输出两个接口IB,IC。  Test2方法需要依赖IB和IC两个接口,因此它有两个成员变量,并加上了标记,标记的内容是该接口的名称。

  下面,我们要做的工作,就是在运行时,自动将test1的方法注入到Test2的内部接口上。

 

四 .  实现内部组装

        当用户点击运行时,系统会自动实现接口装配,并按照执行策略执行列表当中的算法模块。

/// <summary>
    /// 可实现自组织的算法模块设计
    /// </summary>
    /// <typeparam name="T"></typeparam>
     public class SelfBuildProcessMethodCollection<T>:ProcessMethodCollection<T> where T:IProcess
    {

         SelfBuildManager mySelfBuildManager = new SelfBuildManager();
         /// <summary>
         /// 是否设置自动装配算法模块
         /// </summary>
         public bool isSelftBuild = true;
         protected override bool BeginProcess()
         {

             mySelfBuildManager.BuildModule(this);  //实现接口的自动装配
             base.BeginProcess();

             return true;

         }
    }

   在我的系统中,所有算法的抽象接口都是Iprocess,但在这篇文章中,自组织并不一定需要该接口。系统保存的算法保存在了ICollection<IProcess>接口中。而具体装配的方法,则定义在SelfBuildManager中。

public class SelfBuildManager
    {
        /// <summary>
        /// 保存所有依赖接口的字典
        /// </summary>
        Dictionary<string, IProcess> dependDictinary = new Dictionary<string, IProcess>();
        /// <summary>
        /// 可提供接口的集合字典
        /// </summary>
        Dictionary<string, IProcess> outputDictinary = new Dictionary<string, IProcess>();
        public void BuildModule(ICollection<IProcess> processCollection)
        {
            myProcessCollection = processCollection;
            GetAllDependExportDictionary();  //获取所有依赖和能提供的接口字典
            BuildAttribute();   //实现接口自动组装的方法
        }

        ICollection<IProcess> myProcessCollection;
        private void GetAllDependExportDictionary( )
        {
            foreach (IProcess rc in myProcessCollection)
            {
                Type type = rc.GetType();
                // Iterate through all the Attributes for each method.
                foreach (Attribute attr in
                    type.GetCustomAttributes(typeof(SelfBuildClassAttribute), false))
                {
                    SelfBuildClassAttribute attr2 = attr as SelfBuildClassAttribute;
                    foreach (string outputString in attr2.outputInterfaceCollection)
                    {
                        outputDictinary.Add(outputString, rc);
                    }
                    foreach (string dependString in attr2.dependInterfaceCollection)
                    {
                        dependDictinary.Add(dependString, rc);
                    }
                }
            }
        }

        private void BuildAttribute()
        {
            foreach (KeyValuePair<string, IProcess> dependNeeder in dependDictinary)
            {
                IProcess outputProvider;
                outputDictinary.TryGetValue(dependNeeder.Key,out  outputProvider);       //在输出字典中找到满足该依赖项的接口
                if (outputProvider != null)
                {
                    PropertyInfo[] PropertyInfoArray=dependNeeder.Value.GetType().GetProperties();    ///获取该类的所有属性列表
                    foreach (PropertyInfo fl in PropertyInfoArray)
                    {
                        foreach (Attribute attr in fl.GetCustomAttributes(typeof(SelfBuildMemberAttribute), false))
                        {
                            SelfBuildMemberAttribute attr2 = attr as SelfBuildMemberAttribute;   //找到自装配成员的标记
                            if (attr2 != null && attr2.invokeName == dependNeeder.Key)
                            {
                                try
                                {

                                    fl.SetValue(dependNeeder.Value, outputProvider, null);   //通过反射,将提供者注入到需求者的变量中
                                }
                                catch (System.Exception ex)
                                {
                                    XLogSys.Print.Error(ex.Message+"无法进行组装");
                                }

                                break;

                            }
                        }

                    }

                }

            }

        }
    }

     具体的方法请参考代码的注释部分。由于代码注释已经很详细了,因此不做更多解释。

 

五. 实现和验证

     我们将计算方法A和计算方法B都拖入算法执行列表中:

   

     并单击执行按钮:

     

      可以看到,接口确实被正确赋值了。设计成功。

六. 必须考虑的问题和扩展点

     虽然设计成功,但系统有一些不可避免的问题:

  1. 如果一个需求者发现有不止一个满足该需求的提供者,那么如何选择?目前系统未作此区分,仅仅在找到第一个适配对象后停止搜索。合适的方法是提供用户介入的控制方案,即用户可以用线将不同算法的需求和提供联系起来,当然,该需求暂时有些复杂,如果作者实现了它,一定会公开其方法。
  2. 性能和灵活性: 通过反射实现的方法必须讨论性能,好在系统只执行一次装配过程,并尽可能的通过标记简化搜索条件。  但应该研究更好的搜索方法。
  3. 该功能的易用性: 作者本人认为该系统是足够易用的,你可以简单地将需求和提供接口的字符串列表标记在类前,并将需求的接口标记在需求方的成员变量前,暂时没有想到更好的做法。
  4. 相互依赖问题:一种可能的情况是算法A依赖算法B的结果,算法B依赖A的结果,这种情况一定是不允许的吗?不一定,但若能处理这种需求,就可能实现更强的灵活性,同时带来更复杂的组装逻辑。

   有任何问题,欢迎讨论!

时间: 2024-09-20 00:24:49

.NET插件系统(三) 插件间通信问题——设计可自组织和注入的组装程序的相关文章

浅谈C#中一种类插件系统编写的简单方法(插件间、插件宿主间本身不需要通信)

文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:http://www.cnblogs.com/naaoveGIS/. 1.背景 三年多前还在上研时,用C#+反射机制写过插件系统,后来又用MEF写过插件系统.插件系统本身具有易于扩展的优势,所以在实际项目中使用很频繁.即使在B/S项目中,插件的思想也是大行其道,比如前端单页面+AMD编程便可以理解为一种插件机制,以及后台扩展项目统一打包为一个jar放入主系统jar文件中一起发布,也可以理解为插件思想的运用. 这里我们回到C/S插件系统编

Windows平台下C++插件系统实现的几个关键技术问题及其解决思路

根据我的实践,在Windows平台下设计并实现一个C++插件系统,需要解决几个关键技术问题.下面我谈谈需要解决的几个关键技术问题以及我想到的简单的解决思路.由于我主要专注于Windows平台C++程序的开发,这里假设以VS为编译环境,MFC界面库来说明. 1. 主程序和插件的关系问题      插件架构一般可以用下面的图来表示:   (注:此图来自李先静的博客文章:http://blog.csdn.net/absurd/archive/2006/07/04/877063.aspx ,略有修改,特

(3)MEF插件系统中通信机制的设计和实现

文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:http://www.cnblogs.com/naaoveGIS/ 1.背景 一般的WinForm中通过C#自带的Event机制便能很好的实现事件的注册和分发,但是,在插件系统中却不能这么简单的直接用已有的类来完成.一个插件本不包含另外一个插件,它们均是独立解耦的,实现插件和插件间的通信还需要我们设计出一个事件引擎来完成这个需求. 目前很多高级语言中基本都实现了观察者模式,并进行了自己的包装.比如C#中的delegate和event组合

NET插件系统之一,开头:MEF的一些疑问和相关思考

      实现可扩展的软件系统是我一直的目标和想法.可扩展性显然属于动态编程的范畴.因此,几个月来我在业余时间会抽空学习插件系统.   我参考了博客园的几篇插件式系统的文章.知道了实现插件系统有以下的核心流程:       1. 定义插件接口,并在各个功能组件中实现这些接口 2. 在主程序中,通过遍历所需目录下的dll文件,查询实现该接口的type,从而通过createInstance方法实现其动态创建,并加入到主程序插件列表.下面的代码展示了该功能. public void GetAllPl

MongoDB管理工具的插件系统

MongoDB管理工具  MongoCola的开发已经进入第三个年头了. 官方对于C#驱动的投入不够导致了很多东西都必须自己实现,但是不管怎么样,工具现在已经很强 大了. 最近准备着手插件系统的开发,简单的插件系统,其实代码量非常的少. 1.插件基类 插件系统需要一个插件基类的支持,这个基类,规定了一个插件所包含的固有字段,例如插件名称 ,插件说明,插件作者等等. 同时,还定义了主方法的名称,毕竟插件系统肯定要使用反射来完成,所以很多东西必须要统一起 来. using System; names

Win8系统Flash插件无法自动加载怎么解决?

  Win8系统Flash插件无法自动加载怎么解决? 一.按住键盘Win+A+X键打开Windows命令提示符,然后在命令提示符窗口上输入cd C:WindowsSystem32MacromedFlash命令,再回车. 二.接着在命令提示符窗口上输入regsvr32 Flash.ocx命令,再回车.接着在命令提示符窗口上输入cd C:WindowsSysWOW64MacromedFlash命令. 三.然后在Win8系统命令提示符窗口中输入regsvr32 Flash.ocx命令,再回车,接着系统

.NET插件系统之二——不实例化获取插件信息和可视化方法

 面临的问题       在开发插件系统中,我们通常会面临这样的问题:        一些功能并不是在开启时就要被使用的,例如VS中的大量功能对一个大部分程序员来说用不着,但框架本身却应该向用户提供该插件的相应信息?        在可视化的插件功能列表中,我们不仅希望提供简单的插件名称信息,更希望能以图片,或动画等形式展示其功能特性,便于用户选择.        插入辅助类来解决上一个问题? 想法虽好,但破坏了"插件"的精髓,它应该是独立可插拔的,如果存在其之外的辅助类,那真是得不偿

NET插件系统之四——提升系统搜索插件和启动速度的思考

一. 面临的问题 开发插件系统的主要优势是扩展性,我们不需要为系统模块的集成再多费脑筋,但这也带来了额外的问题.通常,系统需要在每次启动时搜索固定目录下的符合要求的插件.但是,当系统变得越来越庞大,所引用的dll文件越来越多时,就会出现很严重的问题:开启时间慢,性能差,用户体验降低,尤其是在调试程序时,会浪费大量宝贵的时间. 我确确实实的面临了这样的问题,有兴趣的读者可以看看我的插件系列文章的前几篇,这两天痛定思痛,决心提升系统搜索插件的性能. 我们先看一段普通的搜索插件的代码: public

插件系统中跨域调用的性能和“一个简单的性能计数器”的使用

系统大概的流程如下:从数据中心接收到数据包(1万~3万(个)/秒,使用WCF)可以被不同的应用场景使 用,每个应用场景的业务逻辑各不相同,而每个场景的接收数据包部分的代码是相同的,于是使用一个容 器将所有的"应用场景的dll"通过反射加载,同时容器接收所有的数据包并把他们分发给"应用场景的dll" ,接收数据的功能,自然被抽象成了Interface.透彻一点讲就是一个插件系统,只不过这个插件有点儿 大而已,大到整个系统都是个插件.这个容器我们暂时叫他"引擎