赵才文
(亚信科技(成都)有限公司,江苏 南京 210003)
随着软件市场的不断开拓,企业开发的软件产品会在越来越多的市场进行部署。一般情况下,为了减少成本,企业会尽量重复使用现有的开发人员和产品代码库。使用一套产品进行部署,虽然能够满足大多数市场的需求,但是某些市场会有自己的特殊需求,需要软件企业为其定制开发某些功能。由于企业的人力资源有限,不可能针对每个市场进行完全定制化的研发,这就必然要求企业具备一种灵活的软件扩展开发方法,在支持产品化研发的同时,也能够支持业务的灵活扩展,以达到通过最少的资源支撑最多市场的目的。
通过研究,目前比较通用的软件扩展方式主要包含表1所列示的六种方式。
通过对上述软件扩展方法的分析,我们可以得到一个结论:最佳的软件扩展方式既有一定的灵活性,又比较容易开发和部署,容易落地,这样的扩展方式才能有效地被推广。即一个好的软件扩展架构应该具有如下特点:
第一,核心模块和扩展点边界清晰。即软件中的可扩展点和不可扩展点的边界应该清晰,开发人员和运维人员容易识别。
第二,扩展点要足够细粒度。要能够提供业务逻辑任意分支点的扩展功能,也就是说扩展点能对应到代码行。
第三,支持多层级的扩展方式。软件的扩展应能够递进地被扩展,即能够实现扩展的扩展,以达到能够基于现有扩展进行再次扩展的效果。
表1 常见软件扩展方式及特点
第四,容易实现,易于配置。能够提供简单的扩展技术方案,适合现在初级开发工程师的开发水平,且能够简单配置。
“插件化软件开发方法”能够较好地解决软件扩展的控制粒度,具备清晰的软件扩展边界。这种技术的主要特点如下:
(1)定义严谨,边界清晰:基于扩展点(Extension Point)和扩展(Extension)的定义实现软件功能的扩展,严格区分软件的核心模块和扩展之间的边界。
(2)与业务逻辑结合紧密且能灵活调整:业务逻辑中任意位置可以定义扩展点以实现扩展,并且能够按照需求的变更进行调整,非常灵活。
(3)支持多层级的扩展方式:可以在扩展的基础上增加扩展点,实现多层级的扩展。
(4)扩展的开发和管理比较简单:通过一个简单的对接口的实现,即可完成功能的扩展,通过一个配置文件实现不同插件的切换,比较容易落地。
插件化开发方法和传统扩展方式的不同,在于当遇到市场需求变化的时候,采用基于插件的开发方式来进行扩展功能开发。
插件化开发中包括插件、扩展点和扩展等要素。插件(Plug-in)是指扩展点和扩展的集合。扩展点(Extension Point)代表软件可被扩展的功能点。采用接口方式定义,可以定义缺省实现,一个扩展点可以有多个扩展实现。扩展(Extension)代表扩展点接口的实现,基于扩展点定义。一个插件可以包含N个扩展点和扩展,一个扩展点可以有N个扩展实现。这些要素之间的关系如图1所示。
图1 插件要素之间的关系
插件通过自定义的类加载方式以支持动态的类扩展能力,具体类调用序列图如图2所示。
图2 类调用序列图
具体调用过程如下:PluginUtils类被调用获取某个扩展点(Extension Point)对应的扩展(Extension)的方法,此类直接调用对应的扩展(Extension)的实例;扩展(Extension)从PluginClassLoader类加载器加载对应的类对象;插件类加载器(PluginClassLoader)获取对象实例后,返回给扩展(Extension);扩展(Extension)返回给PluginUtils,PluginUtils返回给调用者,调用者相当于调用了定制化的插件实现。需要注意的是,其中的PluginClassLoader是一个继承自URLClass-Loader的自定义类加载器,将插件的依赖在运行期动态加入到系统类加载路径中,这样就可以通过loadClass方法获取扩展(Extension)中定义的扩展对象实例。
要配置一个新的插件,需要增加如下配置:
(1)增加plugin.xml文件,定义插件的信息,包括扩展点的定义和扩展的定义,每个扩展点还可以定义缺省实现类。
(2)增加plugin.properties文件,定义项目中使用的插件列表。
对已有的产品,如果要使用插件化开发方法,在设计上要注意下面几点:
(1)对可扩展的业务逻辑抽象出接口,并将原有实现逻辑作为该接口的缺省实现。
(2)调用PluginUtils工具类执行基于插件的业务逻辑。
在某个产品的账号管理模块中,账号添加流程如下:用户登录后,点击账号管理,添加账号完成后,点击提交,账号信息被提交到后台,直接保存到数据库,新账号自动启用。如果此产品被部署到某个新增的市场,用户提出了一个新需求,在添加账号的时候不要自动启用,要先发短信通知管理员,管理员确认同意后才可以启用此新增帐号,以防绕过管理员随意添加账号。这个新增的流程就需要基于已有产品代码进行扩展。
现有的类和实现主要包括三个部分:(1)领域对象层domain层的Account实体类:代表账号实体对象,包含id,name,enabled等基本属性。(2)数据访问层dao层的AccountDao对象:代表数据库访问对象,提供账号对象的增加、删除、修改和查询方法。(3)业务实现层service层的AccountService对象:代表业务逻辑对象,为前台页面提供业务处理方法,例如增加账号的业务逻辑。service层调用dao层提供的方法来实现账号管理的数据库入库功能。
在未引入插件化开发方法之前,通常采用的软件扩展方法是基于类继承的扩展方式。用户提出了账号新增流程的扩展需求后,我们需要对AccountService.java进行改造。(1)新建一个包:service_a包,用于存放A市场的定制需求。(2)在service_a包中新建类AccountService,实现定制化的新增账号业务逻辑:即新增账号缺省为不启用状态,先发消息通知管理员,等管理员确认短信后,再改为启用状态。(3)新增针对A市场的前端账号管理页面,调用新增的service_a包中的定制业务逻辑代码。(4)打包部署到现场后,用户需求得到满足。
通过上述演示,我们很容易想到如下问题:
(1)传统方法采用的是先复制,再修改的方法进行扩展,而且复制的级别是类级别,导致大量的重复代码。
(2)目前是A省提出定制开发需求,如果B/C/D…..省也提出定制需求,那么需要复制的代码就会越来越多,代码重复的情况越来越严重,导致软件越来越臃肿,难以维护。
(3)如果某省觉得A省提出的这个需求很好,也想在本省采用,那么要把这次代码都拷贝一份到B/C/D……省的工程中,工作量很大。
(4)因为复制了大量的代码,如果在复制的代码中发现了一个软件缺陷(Bug),那么开发人员必须在所有复制的代码中修改一遍,修改量很大,然后测试人员也必须在每个版本中测试一遍,测试工作量也很大。
随着市场的增加,这种基于类继承复制的扩展方式必然要求越来越多的人员投入,并且不能快速应对需求变更,随之而来的是用户的满意度降低甚至是市场份额的丢失。
使用插件化开发方式,首先应该将A省定制的账号管理需求变为一个可以重用的插件,并且这个插件是独立的,可以按需配置,这样其他省可以使用“通用版本+功能插件”的方式达到拥有此功能的目的。
为了使用插件开发,必须使用接口(即扩展点)的方式进行方法调用,所以我们将AccountService这个要扩展的类进行接口化改造,增加一个IAccountService接口,同时增加一个插件工具类PluginUtils,提供获取插件实例的方法。表2对每个新增、修改的类和接口进行了基本介绍。
上述用户管理的插件开发场景调用过程如下:
(1)客户端调用AccountService对象的addAccount方法;
(2)AccountService调用了PluginUtils的getExtensionInstance方法,获取扩展点IAccountService当前配置的插件实例;
(3)AccountService获取IAccountService扩展点实例后,调用其addAccount方法实现具体业务逻辑。
通过上述分析,可以看到,调用者并不知道调用了哪个具体插件,只是通过PluginUtils工具类得到插件实例后调用其扩展点接口方法。这样就实现了调用者和实现者的解耦,便于切换不同的实现。
根据上面的描述,我们可以看出插件化软件扩展方法具备下面的一些优点:
表2 插件开发场景关键类和接口
(1)插件的开发可以和原工程的开发分离,这样就可以区分产品核心开发团队和扩展开发团队;也就是说,插件化整体工程的结构分为一个主工程和多个插件工程,在主工程中定义了多个扩展点和缺省实现;而在其他独立的插件工程中,则定义了一些扩展点对应的扩展;这样可以针对不同的开发人员分配不同的代码读写权限,区分产品研发组和插件研发组,将定制开发和主版本开发严格区分开来,有利于进行细粒度的产品研发管控,提升产品开发效率和质量。
(2)概念清晰,具备严格的插件、扩展点、扩展定义方式;在传统扩展方式中,因为没有独立的扩展概念,开发人员需要熟悉各种不同的扩展方式,比如基于配置文件扩展,需要开发人员和维护人员熟悉配置文件的属性定义方式,而这些定义没有一定的规则,容易混乱。插件方式则不同,扩展点和扩展的定义都是在XML中采用固定格式定义,相关人员熟悉非常快速。
(3)扩展点随意选择,选择后进行重构即可使用插件方式;传统的开发方式只能基于已有的API或者配置规则进行扩展,基于插件的扩展方式则可以在任意代码行定义扩展点,也就是说,插件扩展点的控制力度可以在代码的任意行,比传统方式递进了一个层级。
(4)调试和部署简单方便;传统方式下,扩展代码和产品代码揉和在一起,难以区分和管理。插件扩展方式则采用独立的打包方式,也就是每个插件进行单独打包,然后部署,非常容易区分和部署。
当然,插件开发也不是非常完美的方案,目前还有下列的不足:首先是目前还不支持前台页面插件,当前的插件开发主要是针对后台代码进行设计的,还不支持前端页面的插件,这个问题可以用打包的方式暂时解决,即将需要扩展的页面文件、图片、脚本等资源在打包的时候拷贝到主工程。其次目前插件开发还不支持热部署,每次更新插件配置后需要重启整个应用才可以加载最新的插件,这个问题可以通过增加额外的自动定期加载程序实现插件的自动化热加载。
综上所述,插件化开发是一种简便易行的软件扩展方法,通过在软件中定义扩展点和扩展,进而组织为插件进行部署,可以实现软件核心模块和软件定制模块的分离。当然,这种软件扩展方式也有各种不足,需要我们在实践中不断进行完善。不同的软件产品对扩展方式的要求也是不同的,不能一切照搬,需要按照实际情况进行选择。
[1]刘婧.电信行业自助终端支撑系统架构研究[D].西安:西安电子科技大学,2009.
[2]王丽华,王治民,任雁铭,等.插件化I E C 61850通信模块设计与实现[J].电力系统自动化,,2012,36(5):82-85.