zoukankan      html  css  js  c++  java
  • 一次简单的重构经验

    背景

    曾经为一家律师事务所做的案件信息管理工作,使用的是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

    重构策略

    由于是已有的旧工程,要保证重构后功能正常,也要保持重构成本不要过高,策略如下:

    1. 先建立几个模块文件夹(子工程),设定好子工程的依赖关系,然后把相关代码适当地移到到它应该处于的子工程中
    2. 如上描述的Util对象,因为会涉及到不同模块,先重复拷贝到各个模块中,后续再重新定义
    3. 尽量先让编译器帮我做校验,重构失败等于编译失败,这样降低操作成本

    这些策略看来不错,于是我分开了几个子工程:

    1. base:把框架性的代码放到这里,其中包括员工登入等权限控制,Utils等
    2. caseman:案件管理,依赖base
    3. 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 是多么的简单,外包项目,客户都没要求,我要求来干嘛)。但是,只要凭着信念坚持到最后,最终你可以看得到光明。

    这类重构绝对是磨练意志力的练习。

  • 相关阅读:
    Socket
    属性的使用案例
    link.bat
    未命名 (2)
    解决wordpress3.5更新插件和主题失败的问题
    解除文件锁定(此文件来自其他计算机,可能被阻止以保护该计算机)
    String转换成Integer源码分析
    实战体会Java多线程编程精要
    JAVA进阶:一个简单Thread缓冲池的实现
    Java对象的序列化和反序列化实践
  • 原文地址:https://www.cnblogs.com/kmtong/p/3903087.html
Copyright © 2011-2022 走看看