天天看点

.NET实现之(自己动手写高内聚插件系统)

大家看了我这篇文章后,总问我为什么要起个这么怪异的名字“构件”而不用“插件”。其实这个名字在我脑子漂浮了很久,一直找不到合适的场合用它。

一:问题分析

在进行开发之前我们需要对整个系统有个分析,插件系统所强调的核心思想是能让所开发出来的系统应变日常需求,在功能升级的时候能很方便的进行更新。但这不是插件系统的最大的好处,我们用传统的三层、MVC开发也能实现这种好处,无非是将DLL文件放到目录下然后在重启就行了。

但是由于插件系统将功能点分的很细,大部分的功能在没有必要的情况下是不需要操作更新的。东西分的越小越好控制,但是开发的成本也随着控制粒度而变大。所以这个平衡点需要我们自己把握,不是所有的项目都适应这种架构。

插件系统是采用面向接口开发而不是面向类开发,在我们系统需求出来之后需要抽取功能点以进行插件抽象。这个时候就是考验一个项目的架构师的设计能力了。设计的不好导致后期开发无法进行下去,这类问题有很多种,比如:接口定义不明确、返回类型不明确、接口的公共部分是否抽象完全,也就是基类实现的是否合理等等;这些问题都很复杂,真正开发大型系统时,这些问题不能马虎,搞不好项目失败。从需求中抽出插件然后进行概要文档的编写、详要文档的编写。在一些大的方面设计文档可能很实用,但是我们程序员知道,一个设计文档不能通用,不是任何系统结构都能相同的设计文档,这就牵扯到了公司的文档编写方面了。如果设计文档无法应付这些复杂的系统结构,可以由架构师编写项目的架构设计文档,只有这样才能让开发人员一目了然,程序员才能发挥自主能动力能力,才能使项目完美收工。

我们刚才讲了,插件系统是采用面向接口设计、开发,也就是面向对象领域所提倡的开发思想。既然我们是以面向接口设计的,那么我们的插件是完全依赖于某些接口,就好比COM一样,你的接口不变,我就能找到你。最大的好处就是如果我的项目是需要第三方去实现的,那么我们的程序集文件DLL不需要签名,而不能由其他人跟换的插件使用签名,这样系统显的很有柔韧性。我喜欢大师们的开发思想,将自己的项目比作大型的机器人,任何部件是可装配、可更换的。不要将自己的项目开发的那么臃肿,那么脆弱。

插件系统对程序员的自身技术要求也是比较高的,这里面纵横交错,都是需要很深厚的技术功底的。都说这个语言好、那个语言好、只要精通什么都好。这个时候就考验你是否真的掌握了这门语言。语言本身是为了满足某些需求而存在的,JS是为了实现HTMLDOM的交互、CSS是为了修饰HTMLDOM、HTML是一种结构表示语言,这样语言的存在和使用都是有方向的,千万不要把语言和语言相比。由于插件自己的耦合几乎为零,这个时候我们都是通过接口进行调用,比如:我在一个接口里面操作了某些功能,同时这些同能要能及时的反馈到另一个插件中去。这样一个小小的功能,就需要我们运用很复杂的调用关系,任何一步处理的不到位,都会给后期的改动带来麻烦,甚至是灾难性的。

二:真实项目解析

我用了这种结构进行了系统开发,前期的构思是很头疼,但是后期的效果很不错的。

1.主程序实现

在主程序要想使用某个插件的时候我们需要用统一的方法或者说是接口吧,能拿到我这个模块所对应的插件;请看代码:

        /// <summary>

        /// DataSourceOpen插件接口,上下文使用;

        /// </summary>

        BaseCome basecome;

        /// 打开SqlServer数据源

        private void Tools_Sqlmenu_Click(object sender, EventArgs e)

        {

            basecome = NewBaseCome();

            (basecome as DataSourceOpen).PassDataEvent += new PassDataHandler(FrmDbServer_PassDataEvent);

            basecome.StartCome();

        }

        private void FrmDbServer_PassDataEvent(List<string> param, params string[] par)

            if (par.Length > 0)

                if (!IsOpenSource(par[0]))

                    BindTreeView(param, par);

这是我的一个菜单的单击事件,这个菜单是主程序中的功能菜单,我需要在主程序中调用相对应的插件;上面的BaseCome是插件基类,实现了所有插件共同的一些特征,便于调用和实现;我在事件中使用了一个NewBaseCome()方法,这个方式是当前窗体中的公共方法,请看代码:

/// <summary>

        /// 统一获取构件基类

        /// <returns>BaseCome对象</returns>

        private BaseCome NewBaseCome()

            return (PlugManager.PlugKernelManager.MainEventProcess("http://www.emed.cc/CodeBuilderStudio/Details/DataSourceOpen") as BaseCome);

我通过这个公共方法获取到当前功能需要用的插件,PlugManager.PlugKernelManager.MainEventProcess()是插件管理器中的一个共有方法,这个方法会根据你传入的XML命名空间获取配置文件中的插件配置节点名称,你可能会问:“为什么要用这种结构的XML配置文件?”。其实我的个人习惯是使用有结构意义的XML文件,这是其一。其二是,我必须确定插件配置文件的唯一性,由于插件系统支持第三方实现,所以我更本不知道插件的名称是什么,所以我用XML命名空间进行规定。当我需要的时候,我直接通过XML命名空间就能获取到当前插件了。我们一起来看插件管理器的实现,请看代码:

2.插件管理器实现

 /// <summary>

        /// 主程序发生事件,需要启动相应构件

        /// <param name="xmlnamespace">构件所属的命名空间</param>

        /// <returns>本构件加载是否成功true:成功,false失败</returns>

        public static object MainEventProcess(string xmlnamespace)

            try

            {

                PlugDom dom = domcollection[xmlnamespace];

                if (dom == null)

                    throw new System.Exception(

                        "在系统当前上下文构件集合中未能查找出" + xmlnamespace + "命名空间构件,请检查构件配置文件LoadConfig.xml是否进行了相应的设置;");

                ComeLoadEvent(dom.Assembly);//构件初始化成功

                return ReflectionDomObject(dom);//通过反射DLL文件,启动实现构件

            }

            catch (Exception err)

                ComeCommonMethod.LogFunction.WritePrivateProfileString(

                    "MainEventProcess", err.Source + "->" + err.TargetSite, err.Message, Environment.CurrentDirectory + "\\PlugManagerLog.ini");

                return null;

        /// 主程序发生事件,释放构件资源

        /// <param name="comeobject">构件对象</param>

        public static void MainDisposeProcess(object comeobject)

                (comeobject as Main.Interface.ComeBaseModule.BaseCome).Dispose();

                ComeExitEvent((comeobject as Main.Interface.ComeBaseModule.BaseCome).ComeName);

                    "MainDisposeProcess", err.Source + "->" + err.TargetSite, err.Message, Environment.CurrentDirectory + "\\PlugManagerLog.ini");

由于管理器中的代码比较多,我只找了关键的代码。其实插件管理器的主要任务是起到一个衔接的作用,在主程序中通过插件管理器获取到插件对象。

插件管理器的大概实现的功能是这样的,系统启动时读取插件配文件,将配置文件进行对象化,也就是将XML节点进行抽取形成对象,这样便于我们使用。

在用户需要某个插件的时候,我们需要将插件以基类的形式给用户,这样可以消除插件管理器与接口之间的耦合。插件管理器只针对与插件基类。请看代码:

        /// 内部方法,根据Assembly构件宿主程序集名称动态加载内部构件对象

        /// <param name="dom">构件文档对象模型PlugDom</param>

        private static object ReflectionDomObject(PlugDom dom)

                Assembly ass = Assembly.LoadFile(Path.Combine(_comeloadpath, dom.Assembly));

                Type[] entrytype = ass.GetTypes();

                foreach (Type type in entrytype)

                {

                    //所有构件基类,查找构件的入口点

                    if (type.BaseType.FullName == "Main.Interface.ComeBaseModule.BaseCome")

                    {

                        Main.Interface.ComeBaseModule.BaseCome basecome =

                            System.Activator.CreateInstance(type, type.FullName, _comeloadpath, DateTime.Now)

                            as Main.Interface.ComeBaseModule.BaseCome;

                        //注册事件

                        NoteComeLifecycleProcess(basecome);

                        return basecome;

                    }

                }

                throw new Exception("为能实现" + dom.XmlNameSpace + "标识构件,请检查构件配置文件");

                    "GetDomObjectByXmlns", err.Source + "->" + err.TargetSite, err.Message, Environment.CurrentDirectory + "\\PlugManagerLog.ini");

        /// 记录所有构件共有的生命周期事件数据

        private static void NoteComeLifecycleProcess(Main.Interface.ComeBaseModule.BaseCome basecome)

            basecome.ComeStartGoodsEvent += new Main.Interface.ComeBaseModule.OnStartGoodsHandler(basecome_ComeStartGoodsEvent);

            basecome.ComeExitGoodsEvent += new Main.Interface.ComeBaseModule.OnExitGoodsHandler(basecome_ComeExitGoodsEvent);

            basecome.ComeExceptionEvent += new Main.Interface.ComeBaseModule.OnExceptionHandler(basecome_ComeExceptionEvent);

这是插件管理器中比较重要的实现代码。包括反射、事件注册都在这里。Main.Interface.ComeBaseModule.BaseCome 就是插件基类,由于所有的插件需要进行整个生命周期管理,比如释放一些非托管资源、句柄之类的。所以我要进行统一的管理。在此进行事件注册,以方便监听。我们再看一下实现接口的插件代码:

3.插件实现

/*

 *author:南京.王清培

 *coding time:2011.5.28

 *copyright:江苏华招网信息技术有限公司

 *function:开发数据源构件实现,DataSourceOpen.Come项目;

 */

using System;

using System.Collections.Generic;

using System.Text;

using Main.Interface.ComeBaseModule;

namespace DataSourceOpen.Come

{

    /// <summary>

    /// 继承构件基类,没有完全实现构件,继续向下传递实现;

    /// </summary>

    [Main.Interface.Attribute.WheTherNextTransfer(IfNextTransfer = true,

        ChildAssembly = "CodeBuilderStudio.DataSourceOpen.Childe1",

        ChildInterface = "DataSourceOpen.Interface.NextComeInterface")]

    public class ControlContent : BaseCome, Main.Interface.DataSourceOpen

    {

这个插件继承了BaseCome对象,也就是插件基类。然后又实现了Main.Interface.dataSourceOpen接口,当主程序调用的时候就能拿到这个对象了。

总结:插件系统实现大概就讲完了,包扩接口、插件管理器等知识,希望能给各位需要进行插件开发的起到一个抛砖引玉的作用吧。

继续阅读