当一个软件项目开发结束并交付使用后,假如需要增加一些新的功能时,我们希望在不修改原有的应用程序情况下,将新增加的功能"插入"到系统中,这就是所谓的插件化,而新增加的功能模块就叫插件。
插件化技术并不是新的技术,早期很多基于COM的开发的Win32应用程序都是插件化的系统,IE浏览器就是一个典型。而IE浏览器下各种工具栏,如google或yahoo等搜索栏就是插件了。再举个例子,开发工具eclipse。 eclipse最大的魅力在于无限的功能扩展, 很多第三方的厂商都以产品的形式推出实现各种的插件来实现特殊的功能。
插件化系统较传统的软件具有以下的特点:
易于维护
易于维护体现在对插件功能的维护。
当一个系统功能修改或发现Bug, 希望更新最小粒度的模块。 一是解决维护的成本, 二是将修改后的功能对其它完整功能带来的影响降到最低。 在插件化系统中插件一般体现为独立的dll, 这就使得开发人员修改功能时只重新编译相关得dll即可。
易于团队开发
大多数程序员应聘的简历中一般都会有 "良好的团队沟通和协作的能力", 由此可见团队开发中对个人的沟通和协调能力要求的重要性, "沟通协作"的好坏直接影响产品的质量, 那么反过来我们也可以认为: 降低或减少团队中个人之间的沟通和协作势必提高软件产品的质量。。
A和B图分别代表了普通模块划分和插件化模块划分的开发结构,图中箭头代表模块之间的依赖方向,即存在着“沟通和协作”关系。A中,模块之间可能存在较复杂的关系,而B中,各插件模块之间没有依赖关系的,它们只与主框架模块(下面介绍主框架模块)发生关系。
易于功能扩展
任何软件产品都是有生命周期的, 作为程序的创造者,大家都不愿意看到其消亡的现实。 因此应当尽量延长软件产品的生命周期, 不断的将新的功能注入到现有的软件产品是一个手段。 插件化的系统架构为扩展新功能提供了更好的方便性。因为新的功能以插件的形式给出时,是相对独立的,不影响整个系统框架和其它的功能。 甚至是把扩展软件功能的能力提供给用户或第三方厂商,以达到最大限度的功能扩展。
实现插件化的技术支撑
在此之前来探讨一下插件化技术的雏形。 这就必须从库的发展谈起。 库做为独立的功能模块从可执行应用程序分离出来经历了 静态库 和动态库两个阶段,
动态库又随着面向对象技术的发展从用于包装函数到封装类。 如果了解Windows下计算机病毒技术, 可以知道,早期很多病毒制造者依靠系统一个叫 rundll32。exe进程来启动自己的包含破坏行为代码的动态库,来达到病毒在系统中隐藏运行的目的。 而这个rundll32。exe所运行的dll库是包装函数的,只要病毒DLL文件遵循相关的函数名或参数名规则就能被加载和调用。 笔者认为类似这种规定动态库入口函数或者参数名方式来扩充系统功能的技术就是插件化系统的雏形。 在这种情况下, 病毒库可以理解为插件。 COM体系盛行后, 插件技术得到了较成熟的支持, 可以说插件化是COM技术的重要特点之一。 但是受COM自身一些底层实现方式的限制(比如类注册方式,安全机制, 以及臭名昭著的DLL地狱等), 使得在COM平台下开发插件化系统相当的繁琐并且对开发人员的技术水平要求较高。
随着计算机编程语言的发展, 当今流行的Java和C#语言等都提供了新的特性来更方便地支持插件化的系统实现, 在C#中主要依靠接口和类的反射机制两种技术:接口和反射。
接口
接口是插件行为的描述。 接口不包括任何业务逻辑实现,业务逻辑包含在实现接口的类中。
接口声明:
public
interface IPlus
{
///<summary>
///
插件行为方法
///</summary>
void DoSomething();
}
类的实现:
public
class Plus1: IPlus
{
#region
IPlus
成员
void IPlus.DoSomething()
{
MessageBox.Show(GetType().ToString() + " Do somthing...");
}
#endregion
}
插件化系统中提倡通过接口的方式来访问对象,也可以使用虚基类来实现整个系统。使用接口和使用类,在业界也是经常引起争议的话题,笔者比较推荐用接口来实现。
反射
反射主要理解为在未能引用类的类型定义情况下创建和访问对象的能力。
名字空间Kaitu.System中包含了Form类。 一般情况下当需要创建一个Form类的对象时, 必须引用Kaitu.System命名空间, 然后声明Form类型的对象,然后用new操作创建对象。
Form frm = new Form(); //一般类对象的创建。
假如Form类的定义暂时不能引用到当前的开发环境下 , 但时程序在运行期却是能创建并访问它, 这个时候就要利用反射机制来创建该对象:
比如:
using
System.Reflection;
//
加载运行期的程序集。
Assembly
SampleAssembly = Assembly.LoadFile(@"c:/kaitulib.dll");
//
创建程序集中包含的类对象。
object
instance = SampleAssembly.CreateInstance("Guost.Lib.PlusA");
首先是通过程序集的文件名加载程序集对象, 这里类似设计期对程序集的引用。然后是通过类的名称字符串创建类对象,从这里可以看出,创建一个类对象不再用new的方式,而是通过程序集名称和类名字符串创建的。 这里可能有人要问,既然不引用程序集,那么怎样访问创建后的对象呢?用接口,反射创建的类对象都实现了接口,而接口我们在应用程序设计期是可见的。
IPlus
myPlusObject = instance as IPlus; //
将对象转到接口
MyPlusObject.DoSomething(); //
调用业务逻辑
插件化系统基本组成
从上图中可以看出,插件化系统由三个主要部分构成:应用程序主框架,插件配置文档和各种插件。
应用程序主框架
应用程序主框架至少包含描述插件行为的接口定义和插件管理器两个部分。插件管理器包含一个用于存储插件对象引用的列表,或者字典表。插件管理器负责以反射的方式将插件对象创建起来,并保存到插件列表中。并实现一种插件对象的检索和展现机制,即通过应用程序UI部分将系统中已经加载的插件以控件形式展现给用户,(通常是工具栏按钮或菜单项),当用户操作这些控件时,插件管理器又负责快速地检索相应地插件对象并调用其的业务逻辑。
插件配置文档
由于应用程序主框架加载各个插件时通过类反射机制创建对象,这就要求主框架必须识别插件类所在程序集的名称字符串和类。插件配置文档用来存储这些信息。一般情况下当系统增加一个插件功能时,为了使应用程序主框架能够加载这个插件必须在配置文档中对该插件的程序集名称和类名称加以描述。配置文档一般以XML格式存储,也可以根据指定插件程序集的存储路径方式来代替配置文件的形式。
以下是一个典型的应用程序主框架实现代码:
using
System;
using
System.Collections.Generic;
using
System.ComponentModel;
using
System.Data;
using
System.Drawing;
using
System.Text;
using
System.Windows.Forms;
using
System.IO;
using
System.Reflection;
namespace
MainFrame
{
///<summary>
///
插件系统应用程序主框架
///</summary>
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
//
加载插件从当前应用程序目录下检索包含插件的程序集,然后加载。
string
plusPath =
System.Windows.Forms.Application.StartupPath + @"/plus";
DirectoryInfo plusDirInfo = new DirectoryInfo(plusPath);
FileInfo[] fileInfos = plusDirInfo.GetFiles();
for (int i = 0; i < fileInfos.Length; i++)
{
if (fileInfos[i].Extension.Equals(".dll"))
{
Assembly SampleAssembly;
try
{
SampleAssembly = Assembly.LoadFile(fileInfos[i].FullName);
Type[] objTypes = SampleAssembly.GetTypes();
for (int k = 0; k < objTypes.Length; k++)
{
Type objType = objTypes[k];
object
plus = SampleAssembly.CreateInstance(
objType.ToString()); //
反射机制创建对象
plusList.Add(objType.ToString(), plus as IPlus);
//listBox1
的作用是向用户展现系统加载的插件。
this
.listBox1.Items.Add(objType.ToString());
}
}
catch (Exception ex)
{
MessageBox.Show("
创建插件对象失败!
"
+ ex.Message);
}
}
}
}
///<summary>
///
插件字典。
key
为插件的类名,
Value
是插件类对象实例
///</summary>
private IDictionary<string, IPlus> plusList
= new Dictionary<string, IPlus>();
private void listBox1_Click(object sender, EventArgs e)
{
//
当用用户点击
listBox1
时,则根据用户点击的项来调用插件的业务逻辑方法。
string key = listBox1.Items[this.listBox1.SelectedIndex].ToString();
IPlus plus = this.plusList[key]; //
获取插件对象
plus.DoSomething(); //
调用插件对象业务逻辑方法
}
}
}
总结
任何开发技术都不可能是完美无缺的,插件化技术也不例外。插件化的系统无限的功能扩展性并不是绝对的,扩展的插件类必须符合插件接口的定义。而且接口的定义在系统版本升级时不允许修改的,这无疑加大了系统设计时接口的定义难度。
图表 1插件化系统Demo程序运行界面。