天天看點

.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接口,當主程式調用的時候就能拿到這個對象了。

總結:插件系統實作大概就講完了,包擴接口、插件管理器等知識,希望能給各位需要進行插件開發的起到一個抛磚引玉的作用吧。

繼續閱讀