随笔- 34  评论- 226  文章- 11 

我的插件框架·前传

初识

话说很久很久以前,当我还是只菜菜鸟的时候,就说听过关于插件的传说。因为一直都是做富客户端模式的企业应用软件,所以对插件这种神奇的搭积木式的挂接模式,无限向往之。再后来,听说了有关Eclipse的种种神奇传说,与日中天的名气令其在坊间一度被尊为神器,就连俺这么闭塞的.NET粉丝都能把它的名字正确拼写出来,由此可见一番,终于在某个风高月夜伸手不见五指的大白天,俺把罪恶的手伸向了她那被万人膜拜的…

首先找了一些Eclipse的插件开发资料,硬着头皮看了些Java的代码后,始终不得要领,大部分的插件开发资料都属于依葫芦画瓢的讲解方式,而对Eclipse插件架构的整体描述几无涉及,官方的资料中对整体架构的介绍也是粗略含糊,就我这Java门外汉来说,想要通过代码对其有高屋建瓴的理解还真是不靠谱,苦熬之下也便不了了之。然哥早已不在江湖,却依然流传着Eclipse的传说……

时光荏苒,就在某个不经意的日子,突然晴天一声雷,一个名叫SharpDevelop的家伙半道杀出,在.NET社团掀起一阵骚动,趋之者无数,也再次撩动起我不甘寂寞的心,于是乎,历史再度上演…

可怜SharpDevelop官方几乎没有提供有什么价值的文档,网上的资料除了一点介绍性的文字外就剩旁边的广告链接了。也罢,直接看源码吧,也没什么比C#代码更能讲清楚问题的了。SharpDevelop早期0.x版本的代码,坦白说,写得还真是那个啥,跟了跟它的代码,基本流程是清楚了,但是对一些关键问题的疑惑还是没有解决(唉,看来俺的智商还是没有自诩的那么高啊,鄙视一下),问题主要有下:

1、插件节点(PluginNode)与插件(Plugin)以及代码子(Codon)这三角关系究竟隐藏着啥名堂?

2、Doozer(这个不知道咋翻译了)到底主司何职?它到底对Codon负责了什么?如果说他是Codon的构建器,那为啥Codon中还要有BuildItem方法,是不是功能重叠?

3、Codon中的BuildItem方法中还包含有ArrayList类型的subItems,难道是父Codon负责子Codon的创建?父创建子,势必会造成父对子的依赖,如果这样的话,第三方开发人员如果想要基于已有插件进行功能扩展的话,已有插件是不可能了解子Codon的创建行为的,那么这个subItems参数岂不是鸡肋或摆设?再则,如果父Codon只是调用子Codon的BuildItem方法,由子Codon自行构建,那么父如何确保以正确的方式将子Codon挂接进插件树中?

4、插件如何获得环境信息?并如何确保扩展插件不会对依赖插件过度耦合?

5、消息通知机制如何处理?如何将插件自身提供的一些公共信息提供给其他插件共享?插件状态发生改变,如何以一种统一并无耦合的方式通知给感兴趣的其他插件?插件运行时平台如何将事件消息传递给订阅者,在传递过程中,如何以灵活的方式注入进去进行过滤处理?

6、是不是每个Codon都要有对应自己的Doozer?在很多时候,可能需要创建不同的对象,但是这些对象有很大的共性,只是创建过程和后续初始化有差异,如何使用一个Doozer来做到这点?如果一个插件想提供一个服务给其他插件使用,其他插件是不是只能通过接口引用的方式进行解偶?这样的方式,耦合度是不是还可以继续降低?

以上是当初的一部分问题,可能描述的还是笼统了,虽然陆续阅读源码,某些问题有了答案,但是对整个架构的理解还是不能很好的串联起来,故而就算能基于SharpDevelop进行插件开发了,也总有暗箱操作的不安。另外,SharpDevelop中到处充斥着各种条件选择(Condition),尤其是定义某些复杂Codon的时候,各种各样的Condition缠绕四处很是混乱不干净,当时虽说不出具体有啥特别不好,但却总觉有种坏味道隐隐围绕,是不是应该有更好方式来解决Codon按条件进行挂载的问题呢?

带着这些问题,断断续续的纠结了一阵,我的惰性最终在时间的飞逝中发挥出它应有贡献,而江湖的风骚过后大家该吃吃该喝喝,歌舞升平依旧,社会一片和谐,总之形势大好。

 

起因

2008年上,经朋友介绍去到一家Mini型软件公司负责一个管理软件的产品开发,当初该老板介绍的前景不错,待遇也可以,俺屁颠屁颠的带上吃饭的家伙就杀过去了。由于对这个行业不了解以及过度自信,当初也跟着老板喊了不少口号、放了不少卫星(汗一个)。还不错,手下有两个程序员可以使唤(其中一个是从ACCP带过去的学生,小伙非常优秀,2009年底已经出来自己创业了,他们的Ideal非常棒,祝福他们),就这样,我这个技术架构师兼项目经理、主力程序员就这样荣耀登场了。

正如您的高见,我本敲代码者,很多时候不自觉就追求设计的完美去了,等绕了半天之后,才发现原来自己还是个项目经理,还有开发周期在候着呢,更杯具的是,还得去收回当初信誓旦旦跟老板一起放出的那些卫星。通过这个事情,终于切身的体会到一个问题,如果没有制度上的制约,监督者与执行者又是一家人的话,混乱与腐败就是必然的了(呵呵,和谐社会请勿过度联想)。OK,非技术问题在此就不深入展开了,如果是PLMM,则欢迎来电来函来家,彻夜深入探讨,其他者请绕行。

在这个产品中,有个需求:不同的用户登录后,操作界面必须根据其角色进行自动调配,白话说就是用户登录后,如果其是收银员,那么那就只能操作收银窗口及其只能看到或者使用相关的一些功能窗口的入口菜单或者导航树节点。另外,该产品分为不同的发布版本,有精简版、标准版、超级宇宙无敌奥特曼版等等,那么在这些各种版本的发布中包含的功能窗口和模块是不一样的,也就是说,同一种东西要切成不同部分去兜售。看到这里,您或许已经告诉我该怎么做了:在登录成功后的事件中写代码去隐藏相关菜单、导航树节点、主窗口的图形按钮等入口元素,将不同的模块放在不同的Library项目中,发布的相关版本引用相关程序集即可。得幸,当初在下也是这么想的,但是、但是,依咱哥们的秉性,这种傻大笨粗的搞法实在太对不起架构师这个亮闪闪的头衔和Boss当初那饱含热泪殷切期望的眼神了,再怎么地,也得想个稍微对得起咱名声的搞法,嗯,嗯,那个项目周期先缓缓,容俺想想,很快就好的了…

彗星撞地球,果然灵光一闪,插件、全面插件化的架构绝对是彻底、一劳永逸解决这个问题的根本性的宇宙无敌之方案!!!而且,它还可以带来很多别的好处…譬如1、2、3、4… 我总得先说服自己吧.. 嘿嘿,既然这么多好处,那就赶快搞吧。嗯,嗯,那个项目周期,可以再探讨的嘛,这个东西的价值是非常可观的,而且还可以为今后公司所有的产品服务,虽然现在还不知道其他产品在哪,但公司在迅猛发展总会有的噻…

 

初试

既然已经决定要搞,那么怎么个搞法就得先定下来,兵先阀谋还是知道的。做法有二:1、基于SharpDevelop的插件框架来做;2、自己设计开发。

如果采用策略一,首先得把SharpDevelop的核心插件库剥离出来窃为己用,但是必须确保对SharpDevelop有深入的掌控,对其设计思想和架构模式做到了然于胸,以免发生后期无法实现的想法或出现解决不了的Bugs。诚然,要做到如上要求,短期内怕是不太可行,而且有过前些年看其源码的惨痛经历,如今已是心有戚戚焉。

还是采用策略二吧,但首先面临的问题就是,从头搭建一个框架无论是时间成本还是技术风险都是难以预估的,但凡有点良知的兄弟都能看出来,俺要真这么干,就有点利令智昏不知死活了,基本可以拉出去枪毙五分钟。嘿嘿,所幸俺还是能在激动关头保持最后一点理智的,再三权衡,最终决定先做个能满足目前需求的这样一个界面层自动适应功能的类库出来,其他伟大的想法等日后择机再搞吧。

要满足将功能入口点动态挂载到主窗口中去,必须得定义一个配置文件,这个文件起码需要描述挂入的类型(如菜单、导航树、主窗口的桌面快捷图标)、目标窗口的类型、挂载点的显示文本、图标等,不同的角色需要有不同的配置方案,那么可能还要包含一个对挂载配置进行选择的机制。嗯,差不多先就这些了,呵呵,至此俺的想法还是这么纯朴,真为自己骄傲,没有引入其他脱离实际的高调想法进来,一切看起来都在控制之中,不错不错,暗爽一下先。

好了,就从这个配置文件开始吧,有了它,思路就会一步步清晰起来的,顺着这个思路往下理,后面的实现部分就快了。看起来是这个样子:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<configSections>
		<section name="themeManager" type="Zongsoft.Applications.Configuration.ThemeManagerSection, Zongsoft.Applications" />
	</configSections>

	<themeManager>
		<theme name="" resourceName="" resourceAssembly="">
			<menu name="BaseMenu" style="Menu" generatorType="Zogsoft.Applications.Themes.ThemeMenuElementGenerator, Zongsoft.Applications">
				<menuItem name="" text="" icon="" toolTip="" shortcut="" commandType="Zongsoft.Applications.WinForms.ShowFormCommand, Zongsoft.Applications">
					<properties>
						<add name="Command.TargetType" type="System.String" value="Zongsoft.AMS.Basic.FTimeSpan, Zongsoft.AMS" />
						<add name="Command.ShowDialog" type="System.Boolean" value="False" />
						<add name="Command.Singleton" type="System.Boolean" value="True" />
						<add name="Constructor.ModuleName" type="System.String" value="" />
						<add name="Constructor.DefaultDateTimePart" type="Zongsoft.AMS.Common.DateTimePart, Zongsoft.AMS.Common" value="Time" />
						<add name="CustomProperty" type="System.Object" value="" />
					</properties>
				</menuItem>
			</menu>

			<menu name="Navigator" style="Navigator" generatorType="Zogsoft.Applications.Themes.ThemeMenuGenerator, Zongsoft.Applications">
				<menuItem name="">...</navigate>
			</menu>
		</theme>
	</themeManager>
</configuration>

上述配置文件可包含多个主题,主题下面是具体菜单和菜单项,不同的菜单由style属性指定其对应的是菜单还是导航树抑或别的什么,在点击菜单或者导航树的时候,通过菜单项的扩展属性集里面的Command.TargetType项指定的类型来创建对应的对象,可能不同的目标类型需要使用特定的构造函数签名进行初始化,那么通过名称以“Constructor”打头的扩展属性项来指定构造函数的参数定义,当然,如果觉得这些还不够用,可以自定义一个Generator类来解析这些配置内容,将该自定义的Generator类型名称放在菜单元素的generatorType属性中指定即可。需要定义的类,大致如下:

public abstract class GeneratorBase
{
	//初始化生成器,必须指定其对应的
	protected GeneratorBase(Theme theme)
	{
		this.Theme = theme;
		/* ... more ... */
	}

	public Theme Theme
	{
		get;
		private set;
	}

	//根据菜单元素生成对饮的菜单或者树型节点之类的目标对象
	public abstract object Generate(MenuElement element);
}

public class Theme
{
	//该主题对象对应的配置元素
	private ThemeElement _element;

	internal Theme(ThemeManager manager, ThemeElement element)
	{
		this.Manager = manager;
		_element = element;
		/* ... more ... */
	}

	public ThemeManager Manager
	{
		get;
		private set;
	}

	//表示通过GeneratorBase的生成方法后激发,可以在外部进行其他附加处理
	public event EventHandler<ItemGeneratedEventArgs> Generated;

	//调用GeneratorBase的Generate方法生成目标对象,并将该目标对象与caller参数指定的容器进行粘合
	public object Build(object caller, string menuName);

	//对Build方法的复合处理
	public object[] BuildAll(object caller);
}

public class ThemeManager
{
	//根据配置节创建Theme类,并初始化资源服务
	public void Initialize(ThemeManagerSection section);

	//根据指定的资源项名称获取对应的资源对象
	public object GetResourceObject(string name);

	//同上方法的泛型版本
	public T GetResourceObject<T>(string name);

	//获取主题对象集合
	public ThemeCollection Themes
	{
		get;
	}
}

就这么三个核心类,基本可以应付目前这种需求了,代码也比较简单,就不再叽歪了。

 

插曲

.NET社团的老大,就是那个有点微微软的家伙,总喜欢在我们为自己取得一些积累的时候,突然抛一个新玩意出来刺激下我们衰弱的神经,顺带消灭我们那些颇为自得的成绩,而社团内的众多粉丝又都是一帮热衷自残的家伙,于是乎,大家铺天盖日的一番跟风猛跑,杯具的是,俺也是这帮家伙中的积极分子。

就在.NET 3.5发布之际,俺盯上了那个传说中叫作 System.Addin 的美女,不关三七二十八呼啦冲上去就是一阵蹂躏。唉,传说与现实往往是相对的,又或许别人眼中的美女跟自己根本就是阻抗严重不匹配吧,总之,经过俺一番彻底细致的摆弄之后,发现这个东东压根不是俺臆想中的样子,这使得我早已疲惫的心再次惨遭无情痛击。

在 MAF(System.Addin)中,插件与插件宿主之间需要通过严谨的Contract来进行阻抗匹配,宿主与插件内部都有各自的视图用以隔离实现与通讯协定,而各自的视图再通过对应的适配器与之协定进行转换。这个高度繁琐的模型就是为了确保插件与宿主之间阻抗的正确匹配,还可以在不影响宿主或外接程序的情况下更改适配器和协定。想法是好的,设计是糟糕的,在没有亲手写代码去实验的情况下,要想把这么绕的理念整明白,还是有些难度的,为了搞清楚这到底是怎么回事,俺只好先身体力行的按照MSDN上的指引,一步步把Demo重做了一遍,虽然Demo期间我无数次的问候过一些人,但还是咬牙坚持了下来,一个小Demo总共需要7个项目,就算除掉Host之外也还有6个,如果你做完Demo后还能神志清醒的淡定高呼一声“Microsoft”的话,那么恭喜你,本年度最佳盖子奖非您莫属。

 

一不小心,咱也奔三了,梦想中的插件框架一直萦绕心头已成为挥之不去的梦魇。这期间又做过一些项目,对俺上次做的那个简单的主题类库也进行了一番升级改造,但是,底层设计思想决定了高层结构,胚子太丑再怎么拾辍始终还是装不出美女来,不进行基因变异,是没有出路的。

 

前戏

老天爷突然睡醒,一摆手丢了个机会下来,不偏不倚正好砸在白天没啥鸟事,晚上鸟没啥事的本少爷头上 ——嘿嘿,有个哥们找我做个富客户端的企业应用项目。赶紧把俺那套混饭的家伙拾掇拾掇开工干吧,一切按部就班,也没啥叛逆的想法就盼着早点把东西做完交活收钱,然天有不测风云,人有发癫打狂,就在一切顺风顺水的时候,突然地,看着那个主题类库就莫名开始抽经,越看越郁闷,越郁闷就越想废掉它。这次,这个项目时间非常宽松,基本任由着我的性子来,怎么办?要不要搞?到底是要还是要?!呵呵,以前那么多闲淡时间因为没有做项目,就没觉着这个东西还是那么重要,这次切肤之不爽,再不彻底把她整服帖了,以后还是不好混啊,而且MAF这个不靠谱的家伙也让我彻底死心了。

承蒙天意,皇恩浩荡,俺在历经一阵痛苦的抽搐之后,终于痛下决心!某位非著名人士有云:人生的重大事件往往都是短短几分钟内决定的。

 

把玩Unity

根据以往搭建那个主题类库的经验,我首先面临一个对象动态创建和依赖注入的问题,因为各种组件类必须动态创建,这个问题好办,一般使用反射(Reflection)就可以轻松拿下,但是当类之间存在彼此依赖的话,那么这个问题就变得不太好办了,而且在面临多种注入方式(构造子注入、属性注入、方法注入),甚至在构造出对象后,还需要自动调用一些非注入性的初始化方法或者响应处理一些事件的话,这个问题就变得相当棘手了。所幸我对Unity这位仁兄略有耳闻,知道他能够帮我处理这些非插件框架核心但却是基本功能的问题,不过对于如何使用Unity与插件框架进行整合,我还没有清晰的思路,更让我抑郁的是,至今还没听说过SharpDevelop或者Eclipse中集成相关IoC容器的做法,我不确定我这个想法的方向是否正确,而且以前从SharpDevelop的源码中也没看到过类似这么复杂的创建或依赖注入的代码。怀疑终归是怀疑,我还是需要一点论证,至少我需要仔细了解下Unity的内部构造,起码也要看看能不能从另外一个角度帮我理清下思路。

先上博客园找了一些介绍Unity的文章做到心中有数,一番风卷残云般的快速阅读,大致理念弄清楚了。再从Unity的官方网站下载最新源码,对照一些朋友写的剖析Unity的文章(看来跟我一样喜欢扒源码的窥窃男不在少数哈),一边看源码一边对资料,对着对着发现路子不对,因为这些兄弟写的文章基本是依照老版本写的,本着从高从新的伟大理念,也懒得去找老版源码了,就这么凑合看吧,基本把ObjectBuilder的东西看了一遍,大致意思是明白了,主要是Strategies指使Policies去干活,由Lifetime负责管控对象的生命周期和提供容器,一些细节如定位查找等交由Locator去打理,ObjectBuilder只是个对外的前台小妹。UnityContainer内部把对象创建和管理的工作都交给ObjectBuilder处理,自己专注其他辅助功能,譬如配置管理、注入依赖的关联操作并由此引申出来的一大坨杂七杂八的事项。看到这里,基本也没兴趣深挖下去了,因为跟我想要的插件思想没有关系,看来这位仁兄不能带给我额外的思路,而且,如果一个插件框架需要包含一个这么复杂的构建容器是不是有些本末倒置之嫌,这不符合我历来讲究的精巧优雅的架构原则,嗯,可以把它排除了。

 

细探Addin

锁定SharpDevelop,不把她拿下绝不睡觉!

老套路,还是从她的官方网站(http://www.icsharpcode.net/opensource/sd/)下载最新源码,乖乖,现在都3.2版本了,已经长成大姑娘了,嘿嘿~ 文档还是一如既往的精炼,官方论坛的一些零星讨论不能解决我的大问题,只好把那本已经压在床脚多年的《C#软件项目开发全程剖析——全面透视SharpDevelop软件的开发内幕》(http://www.china-pub.com/13944)一书翻出来,在杀死我一些脑细胞之后,除了令我比以前更抑郁外,就是使我更加怀疑自己到底有没有学过中文,这些年说得是不是火星语。

熟练的扒掉她的外套,仔细阅读源码,这次汲取上次的失败,不再从StartUp项目入手而是打开她的AddIn-Scout工具,高瞻远瞩的从整体结构布局入手,一边用该工具查看插件结构,一边对照ICSharpCode.SharpDevelop.addin源文件,一点一点的摸索她的规律,再结合这些年自己断断续续的一些所思所得去推敲她为什么要有一个这样的结构,这些结构究竟是为了解决什么问题,她的核心思想是什么。在深夜里俺不怀好意的死盯她的内部结构,时而跟踪看看类的定义,时而看看对应的Addin文件定义项,时而停下来思考下为什么的问题,终于在自虐了数个不眠之夜、耗光多包‘红色利群’和槟榔后,被俺想通了几个关键问题,譬如那个非著名的暧昧三角关系以及这些内部构件之间的协同作战问题,同时也想明白了为什么插件架构不需要使用像Unity这样的专业IoC框架来处理构件的创建和依赖注入的问题。

 

主题

前面调戏了这么久,再不切入主题就该挨埋怨了。感谢各位东西方仙界朋友的保佑,费了九牛二虎之力把那些关键问题想通之后,再不果断下手就不只是挨埋怨那么简单了,让人怀疑能力有问题那才真麻烦呢。

开搞

在本文的「初识」部分谈到过我对SharpDevelop的一些不爽之处,除此之外还有个令我一直如鲠在喉的不快,那就是对Codon和Doozer这些东西的命名甚感纠结,虽然SharpDevelop开发者在书中也谈到过这个命名问题,但我还是认为这些名字太过生僻。一个好的命名会让人望文生义,这也是我得到偏执者名号的缘由之一。所以,我慎重决定重新厘清这些庞杂系统的概念一并更新命名。首先附上主要的概念性名字解释(含英文名):

  • 插件(Plugin):该英文单词有Plugin和PlugIn的两种写法,前种写法表专有名词,后种为词组,为了照顾编码时书写的整洁度以及.NET通用的命名规则,而采用Plugin写法。插件是插件框架中的逻辑组织单位,可以对应到一个或多个程序集(Assembly),每个插件必须对应一个XML格式的插件定义文件(*.plugin),一个插件由众多构件(Builtin)组成,插件定义文件详细描述该插件内各个构件的组合关系。
     
  • 构件(Builtin):英文Built-in原意为“内置件、零件”,在此将该词组进行合并引申为构件、部件之意,这个意思非常符合其在插件框架中的角色定位,该元素完全等同于SharpDevelop中的Codon。构件为插件(Plugin)的组成部分,其与插件的关系就好比人体器官(如心脏、头、四肢)与人体,其不能脱离插件独立存在,也是基本功能的具体载体,众构件(Builtins)必须通过插件定义文件将其发布到插件框架中。
     
  • 插件树(PluginTree):该英文为词组,遵循.NET开发规范对类名、方法名、属性名等一律使用Pascal命名规范,下同。首先必须高调澄清一个事实,插件树并不是对插件进行数据结构定义的概念,虽然我在这个地方也一度陷入SharpDevelop中的泥潭难以自拔。插件树是对构件(Builtin)对象在内存中的一种树型数据结构的定义,之所以要称之为PluginTree而不是“BuiltinTree”,那是因为它不只是负责管理某一个插件内的所有构件定义,而是全局唯一的,要负责管理整个运行时所有插件内的构件。
     
  • 插件树节点(PluginTreeNode):顾名思义这个元素描述插件树中的某个节点的数据结构,主要包含两个构成:1、该节点在插件树中所处的路径(Path);2、该节点对应的构件(Builtin)。一个节点只对应一个构件对象,通过它我们可以在插件树中任意导航。
     
  • 构建器(Builder):注意区分该中文名词与Builtin的中文名中的“件”和“建”字。该概念大致类似于SharpDevelop中的Doozer,它的唯一功能就是根据插件框架提供给它的构件对象生成真实具体干实事的目标对象。正是因为这个东西的存在,插件框架才能彻底摆脱对庞杂的IoC框架的依赖,因为它非常清楚它要构建的对象是什么。但是,我当初的一个主要疑惑就是当它要构建对象的构造函数参数需要引用插件平台的一些公共服务,譬如当前的登录用户名、或者数据访问服务接口对象,或者该参数还需要引用另外一个插件中的构件对象实例以及这个构件实例中的某个属性值,那么这个构建器如何以一种简单优雅的方式去获得这些依赖对象?再次感谢万能的主,因为插件架构的这种树型数据结构,导致了她天然就有解决这个棘手问题的良方,而且是以一种异常优美简洁的方式来实现的,呵呵,稳住稳住,注意控制好节奏,想想美妙的序曲才刚开始,后面还有高潮部分在等着咱呢。

  • 类型(Type):该元素是本人在设计该插件框架时思忖许久决定引入的一个新概念,跟我设计数据访问框架一样,我的一个原则就是尽量利用.NET中现有或者业界主流设计思想中的概念,非万不得已才引入额外的设计元素,将易用性和广泛适用性原则始终摆在框架设计的最高级别。该元素是为了解决构建器创建构件对应的目标对象时,需要使用到对一些非插件框架服务以及其他插件所能提供的常量型数据的问题。该元素只作为构件的子元素出现在插件定义文件中,是对构件目标类型的一种补充描述。

    好吧,就算可以引入该元素,但我还是要找到能100%说服自己有绝对充分的理由和必要。为什么SharpDevelop中没有包含类似的概念?经过我的一番努力查找源码,发现SharpDevelop这种应用主要是处理开发工具这种需求,在这种应用中基本没有在企业管理软件中一个窗体在构建过程中需要使用一些固定常量的情况,OK,这个理由不够说服力,因为我无法从根本上枚举出所有的需求情景,那么理由二:对于这种简单常量的初始化参数如果也使用构建器中描述的那种依赖注入方式,明显有高射炮打蚊子之嫌,而且对于插件的实现者而言,明显会增加不必要的开发量,更严重的是,它会破坏我一直提倡的简洁优雅的设计和使用理念。呵呵,这个理由足够充分了罢,而且增加这个概念之后并不会给系统带来额外的复杂性和臃肿感,无论对于普通插件开发人员还是框架实现者本人都不会造成额外的工作量,相反它只是简化插件开发的工作并使之更易于理解和配置。

  • 类型别名(TypeAlias):在构件定义过程中,经常需要指定一些没有挂入插件框架的类型(如:System.Windows.Forms.Form, System.Windows.Forms),如果不幸这些类型又需要频繁使用的话,那么在每个地方都重写一遍,无疑是劳民伤财的,与朝廷提倡的建设低碳节能型社会是背道而驰滴,是不与时俱进滴,是没有充分理解带三个表滴,是反XXX…. (汗) 所以,我就让这个概念华丽登场了,应该不会有不知时务者反对吧,呵呵,河蟹好河蟹好。
  • 解析器(Parser):这个对应到SharpDevelop中的解析器类,但是处理手法上与SharpDevelop不同,我把这部分的功能单独出来别于普通构件,让其专司解析之职。它只有一个重要职责:负责将插件定义文件中的某些构件的设置属性的文本解析成它真正需要的东西。听来有点绕口,举例:假如某菜单项构件在插件定义文件中的定义如:
    <menuItem name="Print" type="MenuItem"
    			text="{res:PrintLabel}"
    			icon="{res:Icons.16x16.Print}"
    			shortcut="Control|P"
    			command="/Workspace/Commands/PrintCommand" />
    看到text和icon特性(XML元素的Attribute)中的那些{res:xxxxxxx}字符么咯?Good,恭喜你答对了,Parser就是负责解析这些字符串的家伙,它会返回一些别的什么玩意给构件(Builtin)对象,其中的res:部分就是指定用哪个解析器来解析冒号(:)后面的内容。

 

以上较为详细的介绍了本插件框架中涉及的几个重要核心概念,需要特别指出的是,所有这些元素都是生而平等的,他们在插件环境中都占有同等地位的位置,虽然概念和职责各不相同,但都是为人民服务,身为它们的God,俺是绝对不允许这些家伙中有歧视存在,Never!每个插件结构都可以通过插件树查找到这些元素,并随时为您服务。

 

不妙,有情况

在此,还是忍不住要批评一下SharpDevelop这位国际友人,虽然她如此无私的将她的Body任由我多番蹂躏,也曾陪伴我走过很多个落寞的夜晚,曾让我时而抑郁、时而激昂,时而欢欣、时而捶胸顿足,但不管怎样她都始终一如既往的陪伴着我欢乐悲喜的一路成长。

不幸在一些主要类中看到这样的代码:

static AddInTree()
{
	doozers.Add("Class", new ClassDoozer());
	doozers.Add("FileFilter", new FileFilterDoozer());
	doozers.Add("String", new StringDoozer());
	doozers.Add("Icon", new IconDoozer());
	doozers.Add("MenuItem", new MenuItemDoozer());
	doozers.Add("ToolbarItem", new ToolbarItemDoozer());
	doozers.Add("Include", new IncludeDoozer());
			
	conditionEvaluators.Add("Compare", new CompareConditionEvaluator());
	conditionEvaluators.Add("Ownerstate", new OwnerStateConditionEvaluator());
			
	ApplicationStateInfoService.RegisterStateGetter("Installed 3rd party AddIns", GetInstalledThirdPartyAddInsListAsString);
}
 

这披露了一个性质非常严重的事件,这个 AddInTree 类是SharpDevelop插件框架的核心类,在其静态构造函数中硬编码对某些特殊Doozer和ConditionEvaluator进行初始化动作,这本身就有悖于所有插件结构均一视同仁的插件化理念,正是因为这些少数的特殊化分子导致了后面的插件应用有样学样的一通胡搞。这个问题,我必须得严肃反应一下,虽然我不否认这些个Doozer在整个框架中的重要性,但是就算其功能再重要也不能脱离插件平台的法则约束,所谓法则就是我上面提到的“所有插件结构在平台中都具有生而平等的不容侵犯的插件权”,就算某个元素告诉我他掌管着诸多构件的生杀大权,那依然得遵守‘天赋插权’的法则,总统都还得轮着坐呢。难怪我先前在Addin-Scout工具中怎么都不能在插件树中找到这些Doozer的位置,感情都跑到这里潜规则来了。

 

再来围观一下 WorkbenchSingleton 类中的 InitializeWorkbench() 方法

public static void InitializeWorkbench(IWorkbench workbench, IWorkbenchLayout layout)
{
	WorkbenchSingleton.workbench = workbench;

	DisplayBindingService.InitializeService();
	LayoutConfiguration.LoadLayoutConfiguration();
	FileService.InitializeService();
	StatusBarService.Initialize();
	DomHostCallback.Register(); // must be called after StatusBarService.Initialize()
	ParserService.InitializeParserService();
	Bookmarks.BookmarkManager.Initialize();
	Project.CustomToolsService.Initialize();
	Project.BuildModifiedProjectsOnlyService.Initialize();

	/* ... more ... */
}

还有 CoreStartup 类中的 StartCoreServices() 方法

public void StartCoreServices()
{
	if (configDirectory == null)
		configDirectory = Path.Combine(
                            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
                            applicationName);

	PropertyService.InitializeService(configDirectory,
			    dataDirectory ?? Path.Combine(FileUtility.ApplicationRootPath, "data"),
                            propertiesName);

	PropertyService.Load();

	ResourceService.InitializeService(FileUtility.Combine(PropertyService.DataDirectory, "resources"));
	StringParser.Properties["AppName"] = applicationName;
}

这些特殊分子的榜样力量由此可见一番,其他方面的影响就不一而足了。整个事件给我的感受就有如:当酣畅淋漓正准备放开拳脚大干一番的时候,突然发现某种介质上出现了个洞洞,大煞风景之事,轻者造成一次机体伤害,严重的话,嘿嘿,为此而因噎废食就搞大了。

 

高潮

主题切入这么久了,按理也应该到大家期盼已久的高潮部分了,但是很遗憾,不是每次都能有的,虽然我也很想在这篇文章中,把我设计的插件定义文件作为高潮的产物奉献给大家。无奈,身心俱惫,已经连续苦熬了两个通宵,就俺这个年纪而言,已实属不易。

当自己真正开始插件框架的开发后,里面非常多的细节问题开始逐一浮现,然本文无法一一介绍到,但是,挑战总与成就感结伴同行,上帝与魔鬼将伴随我此后一段相当长的日子,我想这次真的会坚持下去,起码我已能隐约感受到黎明时的温暖。

 

尾声

与日方长,不争一朝一夕,有意者请恕半老不小生不道德的先撤了,后会无期…… bye-bye


作者:钟峰(Popeye Zhong)目前是 北大青鸟(深圳中青)培训中心 的ACCP讲师,负责讲授.NET和SQL数据库开发课程。他曾经使用 C 语言做过图形程序设计,在相当长的一段时期内从事 COM/COM+ 组件的开发和设计工作,并且短暂的做过 Lotus/Notes 和 Dialogic 语音卡程序的开发,从2003年初开始使用.NET这个充满趣味和挑战的开发平台,还领导过.NET平台下的 Windows Mobile 几个项目的开发,对WinForm和WebForm均比较熟悉。感兴趣的除了企业应用架构设计、组件开发、安全、图像处理外还对汽车和枪械模型、边境牧羊犬有浓厚的兴趣。如果希望与他联系,可访问 http://www.cnblogs.com/sw515 或者Email Zongsoft # gmail.com (将#换成@)
posted on 2010-04-23 02:05  钟少  阅读(4805)  评论(31编辑  收藏