本篇主要描述GIX4项目中如何把单独的模块设计为一个“插件”,如何把它组装到系统中。至于为什么加引号,之后会有说明。
原理
在基于产品线开发时,7,2,1的产品功能分类中,20%的功能是需要在产品线主干中包含进来的。这些功能一般会被设计为“可选包”。在某一客户版本产品的装配阶段,在“可选包”集合中挑选需要的功能,进行组装,得到最终的产品。具体内容,见:《软件产品线工程方法:如何在OpenExpressApp做客户化工作》。
在基于OpenExpressApp框架的GIX4项目中,“合同”模块就是属于这20%的功能,它被设计为独立的DLL,在产品装配时为需要的客户进行装配。
DLL间的关系
项目中,实际的开发项目如下图:
图1 解决方案结构图
其中,红色区域的两个项目就是合同模块对应的实体类项目和WPF界面项目。它们都属于“产品721”中的“2”。(最上面的Customizing文件夹中的项目,都是属于各分支的客户版本独有的内容,属于“产品721”的中“1”。)合同包与主干包的关系如下:
图2 合同包与主干包的关系
合同模块中,带有合同信息的预算类ContractBudget从主干版本中的预算书类Budget中继承下来,作为新的聚合根对象(此概念,参见:《DDD》)。
动态加载DLL
在产品线工程的开发中,需要动态加载的DLL,是上述的“721”中的“2” 和“1”。
OEA框架中,使用MEF作为插件框架。(详见金根的:《.Net4下的MEF(Managed Extensibility Framework) 架构简介》)。所有DLL中,实现了IModule接口的
按照约定,把GIX4.Contract.Library.dll 和 GIX4.Contract.Module.WPF.dll 两个dll分别放置到Library和Module文件夹下,框架会自动加载所有的实体类型及其对应的元数据,并按照元数据的内容使用AutoUI模块进行展示。
客户特定的模块,则需要放置在客户各自的文件夹中。这在《基于OEA框架的客户化设计(一) 总体设计》中已经谈过。框架会根据当前的产品定义,进行DLL加载。
把合同包放到项目指定的文件夹中后,按照OEA框架中的元数据信息进行标注的聚合根对象,都会显示在左边的模块列表中,在合同模块中,包含了以下几个根对象:合同模板、合同科目、合同预算导入、合同经济指标。运行界面如下图:
图3 加入合同模块后的软件运行界面
自定义视图
一个独立模块的设计,不会考虑用户是否真的需要其所有的功能。在把它组装进产品后,很可能需要对它进行一些定制。例如,在合同模块的DLL放到产品中后,框架自动加载所有类型并显示,这就导致现在的ContractBudget类和原有的Budget类同时显示出来了。这里我们其实是要用ContractBudget完全替换Budget类,所以,我们需要在产品定义中,把Budget类完全隐藏:
protected override UIInfo DefineUI() { var ui = base.DefineUI(); ui.Entity<Budget>().UnVisible(); ui.Entity<ContractBudget>().Visible(); return ui; }
可以看到,这个定义是直接依赖了合同模块DLL的,也就是说,合同模块不是真的插件,而是在产品编译期已经知道必须包含这个DLL。所以目前只是做到编译期选择装配,而不是运行时动态插入新的DLL,这就是为什么一开始说合同模块并不是真正的插件的原因了。
总结
到本篇为止,客户化的内容已经基本说明。一些其它的问题为以单独的文章说明(例如:实体类继承方式的重构),关注OEA的朋友可以继续关注一下。