分类:
2007-10-15 16:25:59
之前写过不少关于插件系统的文章,有介绍框架的,也有介绍插件结构的。今天主要是分析一下插件系统的组装过程。
组装包括两个部分,界面的装配、插件交互关系的装配。下面会介绍三种组装策略,并简单分析一下不同组装策略的差异。
插件是独立的模块,每个模块向用户提供一部分功能,只有将几个模块组合起来才能真正给用户带来价值。
插件系统分为装载期和运行期两个阶段。
装载期时,装载程序将不同的插件按照配置文件组装起来,主要包括界面布局和插件接口之间的调用关系。
装载完成后的插件系统处于运行期。运行期时需要从两个角度来分析:插件内部行为,包括内部的函数调用以及用户输入的响应;插件之间的交互。
在装载期需要装载器将系统必要的插件按照需求组装成可运行的系统。下面介绍三种组装过程,会采用“积木”的隐喻来解释工作原理。
说起插件不能不说到Eclipse了,它是一个非常成功的插件系统,属于微内核。这里的内核也可以说是装载器了。
首先说两个概念,“插件”和“插件模板”。插件是在系统运行期时的功能模块,从程序员的角度来看,是一些实例对象。插件模板指的是配置文件,有插件本身的配置信息,比如说图标、标题等,另外还有插件之间的关联关系(扩展和扩展点的概念)。可以通过指定的插件模板生成插件。
这里认为Eclipse是最小化的组装是因为在Eclipse中装载过程是分散到系统中的各个角落。由父插件装载并创建子插件,装载并创建就是组装过程。整个Eclipse在装载期只会将配置文件读入内存,生成插件模板体系,随后创建第一个插件(主插件)。至于其他的插件都是主插件的子插件,或者孙子、重孙子辈的插件。装载器的功能非常简单,原因在于它将组装过程分散到系统中的各级父插件的身上。
用积木的隐喻来解释Eclipse的插件组装过程就是,由装载器将第一个积木搭好,然后把设计图纸放到大家都看得到的白板上面。如果有人想看某个积木之上的东西,那么这个积木中自带的一个小的装载机器会将积木之上的其他积木马上放上来。
可以看到,Eclipse中的积木是非常智能的。因为它们本身就会搭积木。这样系统的装载器,要做的事情就非常少了。
装载时需要确定插件的界面呈现,插件之间的交互。如果设计的好的话,插件的外观布局是可以和插件的交互分开处理的。
插件的外观布局由配置文件指定,在装载期,通过读取系统的配置文件可以确定某一插件放在什么地方。在装载期将所有的插件界面全部放置到系统中,这样系统的运行期在界面上和普通的系统没有什么区别——如果需要懒加载,可以用代理对象来实现,对装载期没有影响。
插件之间的交互同样通过配置文件指定。这时候需要用到接口才可隔离不同的插件实现。接口是积木的外观。装载器可以将所有的插件之间的接口绑定起来,可以使用观察者模式来处理。在系统装载的过程中,通过外部的指定,将所有观察者的接口注册到各个目标中,这样目标和观察者之间就可以相互通信了。
这个插件系统就是一个图状的结构,插件自身会存储它和其他插件的关系。装载器的一个重要职责就是将这些关系建立起来,并且维护每个插件内部的观察者列表。
用这种方式组装系统的话,在系统的装载期由装载器就已经将整个积木模型搭建好了。每个积木完成自己分内的事,处理好和自己吻合起来的积木之间的关系就可以。
界面绑定和上面说的没有什么区别,但将插件之间的关系,也就是每个插件内部存储的观察者列表去掉了。这样保证插件本身是最清洁的。
插件与其观察者之间的匹配关系抽象成关系模式,这种对应关系在全局的关系列表中存着。
和第二种对比起来看,界面部分装载没有区别,但第二种组装是将插件交互关系在装载时绑定到插件内部,而这种是将插件关系存成一张列表,每个插件在需要调用的地方动态获得观察者的列表。实现采用的是以前介绍过的关系模式。
装载器也是比较简单的,界面部分不再讨论,交互关系仅仅将配置文件中和交互相关的配置读入内存就可以。
装载就是将现有的积木按照图纸搭成模型的过程。
Eclipse的装载器最会偷懒,他说,我帮你把第一个积木放在那边了,至于其他的积木在用的时候让积木自己装去吧。还好这些积木都是比较智能的,如果积木比较弱智,不懂得装载其他的积木,那么它就只能放在最顶层,上面就不能放东西了。装载器可不会管这些。
装载时绑定的方式,装载器就比较尽心尽力了,他会将所有的插件按照图纸搭好,包括积木的位置、外观等,积木之间相互吻合的地方也是连接的一丝不苟。装载完成后就是一个完整的系统了。当然,这时候的积木就不需要那么智能,也不用管其他的积木怎么摆放,只要把自己交给装载器就行。
运行时绑定的方式中,装载器既不像Eclipse中的那么懒,也没有第二种那么勤快。它将积木的位置放好,外观调好,但是积木之间的衔接处并没有连起来。将每个积木和其他积木的吻合情况生成另外一张连接图,在运行期,由积木自己根据这张图找到吻合的积木。
三种组装策略中都可以实现延时加载的方式。这里将延时加载单独拿出来说是因为这个在插件系统中太重要了。
Eclipse中的延时加载是天生的。装载的策略决定了插件本身就是延时加载的。但后两种却没有这个能力,不过我们能够使用代理插件来实现同样的效果。基本的思路就是,按照配置文件生成插件的代理对象,装载期将所有的代理对象装配起来,在运行期由代理的特性自动装载真实的插件。对于装载器和运行期来说,延时加载是透明的。
第一种的装载过程没有和运行期明确分开,并且插件也会负责装载,所以说这里的职责分配是不明确的,但带来的一个好处就是微内核。其他两种的装载和运行期分的比较清楚,但内核比较大。
在Eclipse中插件的组织是一个树形结构,插件之间的交互非常严格。当然,从系统耦合度的方面来看,这样严格的限制是有好处的,但这种树状的组织结构并不能满足所有的要求,尤其在行业软件中。系统中的插件数量多,交互非常复杂,是一种图状的结构的话,第一种模式就不适合了。第二种和第三种都能满足图状交互的特点,交互的能力强。
如果采用后两种方式来搭建插件系统的话,一定要注意控制系统中的耦合度。
Eclipse的装载最简单,效率是最高的。第二种装载最复杂,效率也是最低的。第三种介于两者之间。
Eclipse运行时需要即时装配插件,运行的效率较低。第二种所有系统运行需要的因素都在装载期处理好,系统运行时都是静态调用,性能最好。第三种介于两者之间。
如果插件之间频繁交互的话,第一种和第二种都是会在插件内部存储一个关联的接口列表,所以性能上不会有问题。而第三种需要动态去取关联的插件,这里需要注意。采用普通的List来存储关系性能上不符合要求,可以考虑使用Hash表来优化性能。
关于插件系统写了八篇文章,虽然有很多细节上没有写出来,但插件系列也算是告一段落了。一方面出于对公司知识产权的保密,现在怎么说都是在给公司做事,讲得再细一些就要把代码放上来了。另外今后我会多放一些精力在我的“读易”系列上面,对于“读易”这一块大家褒贬不一吧,不过我个人觉得还是很有研究价值的。
今后还会零星地写一些纯技术的文章,也希望大家继续关注.Net版块。