插件系统概述
普通的系统,在编译发布之后,系统就不允许进行更改或扩充了,如果要进行某个功能的扩充,则必须要修改代码重新编译发布。使用插件可以很好地解决这个问题。
插件概念
首先由开发人员编写系统框架,并预先定义好系统的扩展借口。插件由其他开发人员根据系统预定的接口编写的扩展功能,实际上就是系统的扩展功能模块。插件都是以一个独立文件的形式出现。
对于系统来说并不知道插件的具体功能,仅仅是为插件留下预定的接口,系统启动的时候根据插件的配置寻找插件,根据预定的接口把插件挂接到系统中。
优势
一、系统的扩展性大大地加强了。如果我们在系统发布后需要对系统进行扩充,就不必重新编译,只需要增加或修改插件就可以了。
二、有利于模块化的开发方式。我们可以开发强大的插件管理系统,在这样的一个插件系统下,我们可以不修改基本系统,仅仅使用插件就能构造出各种各样不同的系统。
Eclipse系统架构
Eclipse插件系统是非常成功的插件框架结构。网上有很多介绍的文章。这里推荐孟岩的Blog http://www.mengyan.org/blog/archives/2005/09/08/67.html。下面对Eclipse的框架中的几点做一个简要的介绍,在后面介绍插件系统架构的时候作为对比。
插件结构
Eclipse是众多“可供插入的地方”(扩展点)和“可以插入的东西”(扩展)共同组成的集合体。在我们的生活中,电源接线板就是一种“扩展点”,很多“扩展”(也就是电线插头)可以插在它上面。(摘自《Contributing to Eclipse》 Erich Gamma, Kent Beck著)
Eclipse整个IDE就是一个插件,他提供了新的扩展点供其他插件来扩展。
扩展点
可以看到Eclipse的插件结构是由父插件管理子插件,插件之间由扩展点连接,最终形成树形的结构。
界面呈现
界面呈现由提供扩展点的父插件来决定,比如说父插件在菜单上留了扩展点,那么子插件就可以出现在菜单项上。界面呈现的类型是由提供扩展的插件决定。
插件交互
插件之间的交互通过扩展点实现。父插件调用子插件实现的扩展点来触发子插件的动作。
依赖关系
配置文件中指定插件运行需要依赖的插件,在装载过程中会按照依赖的关系顺序来装载。
扩展点形成的系统结构
Eclipse中的插件用扩展点的机制连接起来,形成如下图所示的系统结构。插件必须实现扩展点,以此插入到系统中,新增扩展点并不是必须的,但只有新增了扩展点的插件才可以被别人扩展。
懒加载
只有在调用执行动作的时候才会将真实的动作对象创建起来。由于在配置文件中已经具备真实动作的一切信息,所以在不装载插件时,同样可以在父插件的界面上将扩展的功能显示出来。
另一个插件系统
插件结构
插件分为“插件外壳”和“业务”两部分。
其中业务部分与插件没有任何关系,按照一般的应用程序开发即可。最终提供给插件外壳一个主要的界面和公布出来的方法。
插件外壳提供接口供外界调用。系统和其它插件完全通过插件外壳和插件进行交互。
界面呈现
将每个插件的界面按照一定形式组织起来生成整个系统。界面组织的规则在配置文件中指定。系统提供可配置的方案。
布局(Layout)
插件按照一定的布局放到整个系统的界面中。在目前的系统内提供了三种布局。
页面布局
将插件按照页面的形式重叠在一起,插件激活时将自己所属的页面翻转到最前端。
模块布局
将插件按照模块划分放到同一个界面显示。模块之间用分割条连接。
页签布局
将插件按照页签的形式放到一起。
装饰(Decorator)
布局指定了插件出现的位置与形式。装饰可以指定插件出现的方式。
可关闭装饰
指定插件出现的部分是否可以关闭。在普通的模式下,插件可以按照上面的几种版型出现,但这时的插件界面是不可关闭的。如果需要增加关闭功能,可以给插件指定一个装饰器。
下面举一个在模块布局中的模块2上应用“可关闭装饰”的例子。
布局、装饰的组合
上面列举了现有的布局与装饰,复杂界面同样可以有布局与装饰的组合来完成。这里的图式表明将三种布局与装饰组合的一种情况。
通过配置文件指定出不同的组合情况就可以完成更多的界面布局了。在更改整个系统界面布局的时候只需要修改配置文件,程序并不需要重新发布。
导航
通过配置文件装配好的插件系统,界面可能是非常复杂的。这种情况下要让用户找到想要的功能需要用导航器来呈现系统提供的所用功能。
系统提供的功能就是插件提供的功能的集合,插件提供的功能通过插件外壳公布出来。公布的方式依照语言的特性来定:C#、Java中可以利用反射机制运行公布出来的方法,Delphi中用RTTI也可以同样运行配置文件中指定的方法。
常见的导航器都可以抽象成树形结构。每一个导航单元映射到一个用户需要的功能,每一个功能对应到具体的插件的某一个方法。将功能抽象成一个Action对象,对象需要知道它导向的插件和方法名。
可以在上面抽象模型的基础上实现任意形式的导航器。可以是菜单项,可以是TreeView,也可以是自定义的控件。
交互关系
系统需要知道插件的操作,插件与插件之间同样也会有交互。
将所有的交互关系用一个关系管理器来存储,插件与外界交互都通过关系管理器来实现。关系是在配置文件中指定,分析配置文件的时候就会将配置中指定的关系注册到关系管理器中。
在运行期,插件动态从关系管理器中取得和自己关联的接口。
懒加载
为了节省用户资源,需要实现插件的按需加载,也叫懒加载,只有用到的插件才会从文件中装载到内存中运行。
实现懒加载需要处理导航器和插件的布局。很多地方需要绑定插件的信息,但这时插件对象还不存在。使用代理插件可以解决这个问题。
所有与插件的通信都通过代理插件对象来中转。代理对象由主框架创建,记录插件的基本信息。在系统装载期,绑定到系统中的接口都是代理对象,当外界需要与插件交互,例如显示、运行某个方法的时候,由代理来自动装载真实的插件,然后将调用委派给插件来响应。这样可以让懒加载过程对于系统装载,插件运行是透明的。
架构对比
微内核 VS 巨内核
Eclipse中的运行框架非常小,系统中几乎所有的都是插件,采用的是微内核+插件的形式。在后面介绍的插件架构中系统运行框架比较复杂,它包括了界面布局策略、导航、插件代理等职责,可以说是巨内核+插件的形式。
微内核与巨内核之争已经有很长历史了。在操作系统的概念中尤为突出。网上对于微内核与巨内核的讨论同样适用于插件系统。
仅从上面介绍的两种插件系统来看,微内核的好处在于系统的可扩展性强,如果你愿意,甚至可以将Eclipse整个开发环境都替换掉;巨内核的好处在于插件非常简单,只需要将业务部分用统一的接口公布出来就可以,在开发具体模块的时候可以不用考虑开发的是否是插件。
界面呈现
微内核中的界面呈现完全由父插件来决定,留了什么样的扩展点就可以在界面上以什么样的形式发布功能。
巨内核中的界面呈现由系统运行框架决定,框架支持了几种显示的模式。配置文件可以在现有的模式之上随意组合形成复杂的界面。在这个过程中插件并不关心自己被放在什么地方,或者以什么形式呈现。
插件关系
微内核中的插件关系由插件自身来维持,插件实现的扩展决定了它和父插件之间的交互关系,新增的扩展点决定了它和将来在它基础上扩展的插件交互的模式。
巨内核中的插件关系由系统框架(关系管理器)统一管理,插件本身不需要维护交互信息,只有在需要的时候才会从关系管理器取得。
懒加载
两种架构都可以支持插件的懒加载。基本的思路是一致的。但微内核中的插件装载由父插件来完成,而巨内核中的装载则直接由系统框架提供的统一代理类来完成。
========================================================================================================
========================================================================================================
一切都是为了更加简单。
从函数到函数库,然后到类,然后到插件,都是因为我们的软件系统日益复杂,人脑毕竟有限,不能同时处理那么多的信息量,所以采用分而治之的方法来管理。
今年已经研究了一年的插件系统,从最开始的懵懵懂懂到现在能有些经验和大家分享,这个过程本身就是很有意思的。
最开始系统中有了十几个插件,经过几个月的慢慢发展,到了大几十个,甚至上百个,这个数量就有些令人头晕了。不过更加麻烦的还不是这近百个插件组装而成的系统,而是某一个插件系统需要调用另外的一个或多个插件系统。这样的话,插件的数量就在100的基数上开始翻倍。
如何做插件系统中的整合成了一个紧急的课题。
一、插件系统基本结构
前面写过一篇文章,说到了插件系统中的微内核与巨内核之分。不过不管是哪一种,任何一个系统都需要有一个启动点,只不过对于插件系统中的启动步骤来说,它是一个通用,并且和具体业务无关的独立模块。
可以按照下面的图示来简单理解插件系统:
图中的Launcher是插件系统的启动模块,EntryPoint是系统的入口点,作为一个接口给Launcher调用。启动模块通过EntryPoint将系统运行起来。系统中的插件相互协作满足用户的需要。
二、开始集成
上面的图将一个插件系统的基本原素描绘出来了。在具体的项目中,这样的一个插件系统中插件的数量可能多达上百个。当两个项目组都在开发各自的产品,项目组A需要将项目组B开发的系统集成到自己的系统中时,就要开始考虑集成的问题了。
系统中的插件之间存在父子关系,任何一个插件都可以作为另外一个插件的子插件存在。
如果将系统B作为系统A中某一个插件的子插件是不是就可以解决集成问题了呢?——不错,一个简单但实用的解决方法。
可以将插件系统考虑成一个函数库,函数库中的几百个函数相互协作完成一系列复杂的功能。现在我们需要在自己写的函数中包含上面函数库中的所有功能怎么办,简单的做法是将函数库中的某个入口函数作为子函数调用就可以了。
下面介绍的集成方案基本上就是这个思路。
三、插件系统集成解决方案
3.1 EntryPoint与Endpoint
EntryPoint是插件系统的启动模块调用系统功能的接口,这个接口是非常简单的,很多时候仅有一个Run方法,直接对应到用户的双击打开程序的操作。
在系统A中要调用系统B时,显然一个简单的Run方法不能满足要求,这里另外提出一个系统的入口点(端点)Endpoint。
两者的区别在于,EntryPoint对应到Launcher的启动过程,参数简单;Endpiont对应到其他系统的交互过程,参数复杂,需要通过Endpoint传递其他系统需要的信息。
3.2 BUS
有了每个系统的端点,还需要将这些端点组合起来,保证插件系统之间的相互通信。类似于电脑中的总线概念。一旦每个系统的Endpoint挂接到了总线上,插件系统就可以通过总线查找到自己需要交互的其他插件系统了。
这里的总线用关系管理器来实现。因为Endpoint在插件系统中也是作为一个插件存在,这个插件的职责就是和外界交互。关系管理器可以处理任何插件之间的交互,尽管插件并不在同一个系统中。
3.3 Linker
在系统A中呈现系统B的功能有多种表现形式,比如说在系统A的某个地方放上一个Button,点击后系统B出现;或者在系统A中放上一个页签,和一般功能并列将系统B呈现在系统A中。不管怎样呈现,可以将系统B看作系统A的一个插件。这个插件就是图中的Linker。
Linker是系统B的一个代理插件,它本身并没有实现业务,只是将与系统B的交互以插件的形式呈现在系统A中。Linker通过总线找到对应的插件系统并将它启动,同时负责与它的交互。
四、适配器模式
Endpoint用的是Adapter的思想,将自身系统的功能以规定好的交互方式发布到总线上,这样其他插件系统才能与之进行交互。这种方法在系统的集成中用得非常多,已经从设计模式上升到了架构模式的层次。
有了这种适配器的方式,不仅仅是插件系统可以集成,甚至非插件系统同样也可以集成到插件系统中来。所作的就是需要给非插件系统提供一个Adapter插件。对于其他插件系统来说,这个非插件系统在BUS的表现也和插件系统没有差别了。
五、说时容易做时难
上面提出了一种插件系统的集成方案,目前正在逐步的尝试,过程中还遇到了一些细节上的问题,今后等我慢慢整理出来再和大家分享。
现在只是做了插件系统与插件系统之间的集成,虽然从理论上说,插件系统与非插件系统的集成也同样可行,不过目前还没有实践,不敢妄下定论。等有了机会再好好研究一下这方面的内容。如果哪位朋友有一些好的经验愿意分享,在下洗耳恭听:)
========================================================================================================
========================================================================================================
插件可以封装一定的业务,同样控件也具有封装性。
可以说控件的出现大大简化了我们开发的工作量。作为一个插件系统来说,实现一个通用的插件能在更大粒度上进行复用。插件是比控件更加高层的一种模块封装方式。
插件和控件有相同的地方:封装和复用。本文分析了它们的异同,并且提出另外一个比较有趣的概念——伪插件。请大家继续往下读一读。
一、插件和控件的比较
发布
控件编译到系统中,和系统作为一个整体发布。
插件是在系统的运行过程中动态关联到系统上,可以和系统的其他部分保持物理上的隔离。
配置能力
控件在系统中的呈现方式在编译时已经确定,通过代码描述控件的表现形式,呈现位置等。
插件的呈现方式在运行的时候根据外部的配置文件指定。
功用
控件作为公用的组件使用,在我们编写业务模块时,控件作为基本资源被我们使用。
插件作为一个独立的业务模块存在,直接面向用户。
开发调试
控件的调试简单,但插件的调试却比较麻烦。正是因为为了灵活性而制造的隔离措施导致了调试上的困难。通常一个插件作为一个工程开发。
二、插件与控件的关系
插件是业务模块,就像上面所说的,在我们编写业务模块时控件作为基本资源被使用。所以插件与控件的关系如下图左所示,普通的业务模块如下图右所示。
可以看到,插件是满足一定接口协议的业务模块。
三、混乱的界限
作为控件使用的插件
如果一个插件中只有一个控件,并且没有其他的业务逻辑。这种情况下它是插件还是控件?
就像上面所说的,插件是带有一定业务的模块,并且是直接面向用户作为一个系统功能来体现的。插件仅仅是封装了一个控件,并没有带有其他的业务。像这种模块是作为其他插件的子插件使用。如下图所示。
这和我们上面看到的插件内部直接包含控件就不一样了。控件作为子插件的形式被其他插件使用。
插件的配置文件中会将自身的属性作为配置,如标题、图标、和其他一切可以作为配置的元素。但子插件没有详细的配置文件,它的属性直接通过插件的接口暴露给父插件。
这类的子插件是介于插件与控件之间的“伪插件”,因为它并不能独立地在系统中运行,并且通常情况下不带有业务逻辑,不能直接给用户带来价值。
发布后可更换控件
伪插件似乎没有什么好处,谁会无缘无故地在控件之上再封装一层作为插件来使用?
可以想象一下,在系统发布后,我们需要改变某些插件中使用的控件。当然,可以将那些插件全部重新编译后发布。但如果使用这种“伪插件”的思路,我们可以开发一个满足同样接口的另外一个伪插件,并在内部使用不同的控件实现。这样就可以在不发布其他插件的情况下,灵活地修改我们使用的控件了。
额外开销
如果所有的控件都像上面的来实现,那简直是一场恶梦,并且也没有这个必要。因为这样做的成本比较大。
至于实际中是直接用控件,还是用伪插件的技术,那就要看我们的决策了。