首页 > 代码库 > 一次简单的重构经验
一次简单的重构经验
背景
曾经为一家律师事务所做的案件信息管理工作,使用的是Playframework 2.3.x / Java。由于是外包项目,原来就只是一个工程,也没有打算再拆分子模块。
后来这家公司继续为系统考虑添加功能,要增加一系列的CRM中的销售管理的功能,问题慢慢浮现。
我发现问题有几个:
- Playframework本来就能进行代码修改、编译、加载、运行,一直以来都非常方便,但是开始CRM部分工作,这个修改到运行的周期开始慢了。平时修改完代码保存后,到运行有结果,只需要几秒钟的时间。到最近CRM开始了,修改一个源代码档案,编译器会帮我编译几十个,甚至上百个代码档案,我开始怀疑依赖关系混乱了,导致编译器很难判断那些档案需要重新编译。
- 由于没有模块的概念,我在项目中养成了一个小小的坏习惯,就是把我认为是[一般]的功能函数放在Utils对象里。本来如日期的格式化之类的功能放在Utils里无可厚非,但是如果把一个Contact的名字(Salutation FirstName,LastName)都用Utils来格式化,结果是Utils庞大而且分工不清晰。其实理论上,这些函数可能是属于CRM的特有的功能,因为Contact在CRM功能改造才浮现的一个名词,用CRMUtils对象会明确一点。
- 打开一个工程,我开始无法简单的关注我需要改动的部分,虽然可以用Eclipse的Filter等来选择掩盖package等,这个对于未来的改变不利,会增加未来可能加入的新人的学习成本。
我需要改变,改变思路是模块化。
以下部分不仅仅限于Playframework,理念基本上是通用的。无论你用的是Java/Maven,还是.NET
重构策略
由于是已有的旧工程,要保证重构后功能正常,也要保持重构成本不要过高,策略如下:
- 先建立几个模块文件夹(子工程),设定好子工程的依赖关系,然后把相关代码适当地移到到它应该处于的子工程中
- 如上描述的Util对象,因为会涉及到不同模块,先重复拷贝到各个模块中,后续再重新定义
- 尽量先让编译器帮我做校验,重构失败等于编译失败,这样降低操作成本
这些策略看来不错,于是我分开了几个子工程:
- base:把框架性的代码放到这里,其中包括员工登入等权限控制,Utils等
- caseman:案件管理,依赖base
- crm:新的CRM工程,依赖caseman和base
但是我预期需要处理的不仅仅是把代码各就各位那么简单。其他需要处理的重要部分还有:
- 当分开了模块后,主功能菜单属于那一个模块呢?
- 注入框架的配置(我用了Guice),在那个模块启动呢?
原来这两个问题才是核心问题。
重构过程
整个代码搬迁过程看来都比较顺利,直到要把主菜单归到相应的项目的时候。
主菜单
深入想想:主菜单是什么?当一个功能模块加载后,主菜单会发生什么变化?
我希望做到的是:子模块可以[贡献]部分主菜单的显示项。贡献这个字,来自英文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);}
模块需要实现这个启动接口,作为相当于整个模块的[入口]。
启动代码
回顾一下,我们有什么东西:
- base,caseman,crm模块,和相应的 IModule 实现
- IModule需要实现Guice的菜单使用的Multibindings配置,主菜单搞定
任何应用都有启动的步骤和结构,在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 是多么的简单,外包项目,客户都没要求,我要求来干嘛)。但是,只要凭着信念坚持到最后,最终你可以看得到光明。
这类重构绝对是磨练意志力的练习。