一、前言
插件,意味着可擴展,且宿主程序不依賴於插件,即插即用。這種軟件設計方式可以使我們的應用程序最大化地獲得可擴展性、適應性和穩定性,而且便於軟件的維護和升級。在什么場景下使用插件呢?例如在本篇文章中,我個人有一個小需求就是希望記事本帶行號,於是我自己寫了一個極簡易的編輯器(CodeEditor),以這個編輯器為例,主體程序功能包括常見的新建、復制、查找、保存等已經完成,但是在使用的過程中發現需要用到 格式化 這個功能,但是我還不想再去改主程序,這種情形下就可以通過插件來實現,這樣以后在使用的時候,只要有新的需求就可以通過新增插件來實現,從某種程度上講這也符合了開放-封閉的設計原則。下面對插件的定義來自百度百科。
插件(Plug-in)是一種遵循一定規范的應用程序接口編寫出來的程序。其只能運行在程序規定的系統平台下(可能同時支持多個平台),而不能脫離指定的平台單獨運行。
二、插件機制實現原理
實現插件機制的兩大要素:一個是接口,另一個是反射。接口其實是一種“契約”,主程序是通過這種“契約”來約束是否存在符合我期望的對象,如果不符合就不會去加載該對象。在CodeEditor中我們約定的接口是IExcutable。而這種“契約”的執行就是通過反射來達到目的,主程序中會通過反射加載約定好的Plugin文件夾下所有的DLL文件,然后遍歷這些插件並查看是否存在實現了IExcutable的並且可以實例化的類,如果有則創建該類的實例加入集合並返回集合。主程序拿到集合后會在構造函數中加載這些插件,加載過程包括動態添加菜單、指定菜單的點擊事件,這樣完整的插件加載過程就完成了。下面通過CodeEditor來具體看下插件的實現過程。
三、插件機制的實踐
下面的圖是整個CodeEditor的目錄結構

第三個CodeEditorControl可以忽略,這個類庫是一個自定義的控件,是實現一個帶行號的文本編輯器的核心組件,但是和本文主題關系不大。主要看插件接口CodeEditorInterface和插件實現CodeEditorPlugins以及主程序CodeEditor。這三者的關系可以通過以下圖片來展示。

首先從主程序和插件之間的橋梁入手,就是插件的接口,在CodeEditorInterface中的接口IExcutable中有兩個約定方法,一個是GetName負責返回當前的插件名稱,用於主程序獲取並動態加載到菜單中;另一個是Excute負責獲取主程序中文本並執行相應的操作。代碼如下:
1 public interface IExcutable 2 { 3 //用於主程序動態創建菜單 4 string GetName(); 5 //執行具體的文本操作 6 string Excute(string text); 7 }
下面是主程序加載符合“契約”的插件對象的核心代碼,主要作用就是過濾符合接口的類並實例化類的對象,加到集合中:
1 public class Common 2 { 3 /// <summary> 4 /// 加載插件 5 /// </summary> 6 /// <returns></returns> 7 public static List<IExcutable> GetPlugins() 8 { 9 List<IExcutable> implementObject = new List<IExcutable>(); 10 //獲取項目根目錄下的Plugins文件夾 11 string dir = GetPluginsDir(); 12 //遍歷目標文件夾中包含dll后綴的文件 13 foreach (var file in Directory.GetFiles(dir + @"\", "*.dll")) 14 { 15 //加載程序集 16 var asm = Assembly.LoadFrom(file); 17 //遍歷程序集中的類型 18 foreach (var type in asm.GetTypes()) 19 { 20 //如果是IExcutable接口 21 if (type.GetInterfaces().Contains(typeof(IExcutable))) 22 { 23 //創建接口類型實例 24 var IExcutable = Activator.CreateInstance(type) as IExcutable; 25 if (IExcutable != null) 26 { 27 implementObject.Add(IExcutable); 28 } 29 } 30 } 31 } 32 return implementObject; 33 } 34 35 /// <summary> 36 /// 獲取插件目錄 37 /// </summary> 38 /// <returns></returns> 39 static string GetPluginsDir() 40 { 41 string pluginDir = ConfigurationManager.AppSettings["pluginDir"]; 42 return pluginDir; 43 } 44 }
下面的代碼段主要功能是在主程序中為插件分配菜單,綁定公共事件:
1 /// <summary> 2 /// 創建插件公共事件 3 /// </summary> 4 /// <param name="sender"></param> 5 /// <param name="e"></param> 6 private void Plugin_Click(object sender, EventArgs e) 7 { 8 ToolStripItem item= sender as ToolStripItem; 9 if (null != item) 10 { 11 12 if (null != item.Tag) 13 { 14 IExcutable plugin = item.Tag as IExcutable; 15 if (null != plugin) 16 { 17 CodeContent.RichText=plugin.Excute(CodeContent.RichText); 18 } 19 } 20 } 21 } 22 23 /// <summary> 24 /// 主程序加載插件 25 /// </summary> 26 private void LoadPlugins() 27 { 28 List<IExcutable> list = Common.Common.GetPlugins(); 29 foreach (var Iplugins in list) 30 { 31 ToolStripMenuItem item = new ToolStripMenuItem(Iplugins.GetName());//動態創建以插件菜單 32 item.Name = Iplugins.GetName(); 33 item.Click += new EventHandler(Plugin_Click);//綁定公共事件 34 item.Tag = Iplugins; 35 this.Plugins.DropDownItems.Add(item); 36 } 37 }
其中的GetPlugins方法就是遍歷指定目錄下的DLL文件,並把符合接口約定的對象加入集合返回給主程序。而GetPluginsDir方法是獲取插件的存儲位置,主要是在配置文件中讀取插件目錄。
1 <?xml version="1.0" encoding="utf-8"?> 2 <configuration> 3 <startup> 4 <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/> 5 </startup> 6 <appSettings> 7 <!--配置加載插件目錄--> 8 <add key="pluginDir" value="CodeEditorPlugins"/> 9 </appSettings> 10 </configuration>
實現效果如圖:


轉換前的文本,Format的作用是把所有的小寫字母轉為大寫。

轉換后的文本。
四、總結
這個迷你編輯器是之前的一個小程序,整理代碼的時候發現的,突然想改造一下使其更符合我的使用要求,就順便加了個插件機制。插件機制是一種良好的軟件設計思想,可以在不修改主程序的情況下擴展主程序的功能,有時候一款軟件的插件功能要比主程序自帶的功能要強大得多。應用插件機制要注意幾點:
- 定義接口,也就是主程序與插件的“契約”
- 應用反射,通過反射來加載符合接口的類,然后創建該類的對象調用接口方法
代碼整理完畢,已經開源到GitHub上,希望這篇文章能幫助到對插件機制不是很了解的人。如果文中表述有不得當的地方,還請指正。
作者:悠揚的牧笛
博客地址:http://www.cnblogs.com/xhb-bky-blog/p/6287973.html
聲明:本博客原創文字只代表本人工作中在某一時間內總結的觀點或結論,與本人所在單位沒有直接利益關系。非商業,未授權貼子請以現狀保留,轉載時必須保留此段聲明,且在文章頁面明顯位置給出原文連接。
