曾经为一家律师事务所做的案件信息管理工作,使用的是Playframework 2.3.x / Java。由于是外包项目,原来就只是一个工程,也没有打算再拆分子模块。
后来这家公司继续为系统考虑添加功能,要增加一系列的CRM中的销售管理的功能,问题慢慢浮现。
我发现问题有几个:
我需要改变,改变思路是模块化。
以下部分不仅仅限于Playframework,理念基本上是通用的。无论你用的是Java/Maven,还是.NET
由于是已有的旧工程,要保证重构后功能正常,也要保持重构成本不要过高,策略如下:
这些策略看来不错,于是我分开了几个子工程:
但是我预期需要处理的不仅仅是把代码各就各位那么简单。其他需要处理的重要部分还有:
原来这两个问题才是核心问题。
整个代码搬迁过程看来都比较顺利,直到要把主菜单归到相应的项目的时候。
深入想想:主菜单是什么?当一个功能模块加载后,主菜单会发生什么变化?
我希望做到的是:子模块可以[贡献]部分主菜单的显示项。贡献这个字,来自英文Contribute。当我将来要写一个会计模块的时候,我只需要添加模块,主菜单就会自动添加了会计功能项。
(顺带一提:业务方可能永远无法提出这样的动态菜单的需求,因为这个不是业务需求,大概只能在重构过程中又技术团队发现这个需求来。)
主菜单我把它放到base工程,当没有其他模块的时候,空空如也。
每一个模块如果需要添加一个主菜单项的时候,需要实现一个主菜单贡献类:INavigationProvider(接口定义在base,实现定义在各个模块),模块实现方把菜单部分的HTML定义好,由base去调用获取。
public interface INavigationProvider { /** * Define the position of this menu item. */ Integer getOrder(); /** * Actual HTML fragment of the menu item. */ Html getFragment(NavigationItemLocation location); }
当然,由于刚才定义的base也是管登入的权限,base因为也是一个模块,也可以贡献一个登入功能菜单。
由于我们用的是Guice,用Guice的Multibindings就最适合了。
一个模块不仅仅是一些对象的集合,还可能包括一些配置,如上说的主菜单的Multibindings的配置,和一些启动需要初始化的东西。
这些都需要定义模块启动时候的约定,如定义一个模块接口,有:onStart()、onStop()。定义了这两个应用层面的生命周期方法,感觉就对了。
public interface IModule { /** * Define the Guice Module used for configuring this Application Module. */ Module getModule(Application application); /** * Multibindings for contribution to the main menu. * */ void config(Multibinder<INavigationProvider> navBinder); /** * Called on application start, after module initialization. */ void onStart(Application app, Injector injector); /** * Called on application stop */ void onStop(Application app, Injector injector); }
模块需要实现这个启动接口,作为相当于整个模块的[入口]。
回顾一下,我们有什么东西:
任何应用都有启动的步骤和结构,在Playframework,这个在Global.java。启动部分可以看到,相当简洁:
IModule[] modules = new IModule[] { new BaseModule(), new CaseManModule(), new CRMModule() }; public void onStart(Application app) { List<Module> guiceModules = new LinkedList<Module>(); for (IModule m : modules) { guiceModules.add(m.getModule(app)); } guiceModules.add(new AbstractModule() { protected void configure() { Multibinder<INavigationProvider> nav = Multibinder .newSetBinder(binder(), INavigationProvider.class); for (IModule m : modules) { m.config(nav); } } }); // initializing injector this.injector = Guice.createInjector(guiceModules); for (IModule m : modules) { m.onStart(app, injector); } }
启动代码就只有一个对象,现在在主项目的对象,严格来说只有这一个对象,其他对象都散落在各个模块中。
如果要再能动态一点,把modules的定义通过其他方式动态加载进来,如:Java Service Provider Interface之类,把一个JAR文件掉到classpath就行了。
不过这样子我已经非常满意了。
在整个重构过程中,代码迁移到跑又跑不动,一个高层对象被它的底层对象应用,对象的循环依赖,不能编译,一切一切...... 我想过放弃 (只是一个 git branch -d 是多么的简单,外包项目,客户都没要求,我要求来干嘛)。但是,只要凭着信念坚持到最后,最终你可以看得到光明。
这类重构绝对是磨练意志力的练习。