重构遗留程序的一次案例学习(3)

发表于:2013-12-20来源:InfoQ作者:Chen Ping点击数: 标签:重构
at java.io.FileInputStream.(FileInputStream.java:106) at java.io.FileInputStream.(FileInputStream.java:66) at java.io.FileReader.(FileReader.java:41) at com.foo.bar.config.ServerConfigAgent.parseFile(

  at java.io.FileInputStream.(FileInputStream.java:106)

  at java.io.FileInputStream.(FileInputStream.java:66)

  at java.io.FileReader.(FileReader.java:41)

  at com.foo.bar.config.ServerConfigAgent.parseFile(ServerConfigAgent.java:1593)

  at com.foo.bar.config.ServerConfigAgent.parseConfigFile(ServerConfigAgent.java:1720)

  at com.foo.bar.config.ServerConfigAgent.parseConfigFile(ServerConfigAgent.java:1712)

  at com.foo.bar.config.ServerConfigAgent.readServerConf(ServerConfigAgent.java:1581)

  at com.foo.bar.ServerConfigFactory.initServerConfig(ServerConfigFactory.java:38)

  at com.foo.bar.util.HibernateUtil.setupDatabaseProperties(HibernateUtil.java:207)

  at com.foo.bar.util.HibernateUtil.doStart(HibernateUtil.java:135)

  at com.foo.bar.util.HibernateUtil.(HibernateUtil.java:125)

  看起来只要找到server.conf文件就可以了,但这种方式让我有些不爽。仅仅才编写了一个简单的测试用例,就暴露出了代码中的一个问题。正如HibernateUtil的名字所建议的,它仅仅关心数据库信息,而这些信息应该都由database.properties文件提供,为什么还需要访问用以配置服务端启动信息的server.conf文件呢?这里似乎暗示代码散发出坏味道了:当你感觉到自己如同在读一本侦探小说,不断地问“为什么”的时候,这就意味着代码是糟糕的。如果我再多花一些时间完整地看一下ServerConfigFactory、HibernateUtil和ServerConfigAgent这些类,大概能够找到让HibernateUtil直接使用database.properties的方法吧。但那个时候的我已经心烦意乱了,始终没法让程序启动。顺便说一句,这里有一种处理它的临时方案,这把武器就是AspectJ:

  void around():

  call(public static void com.foo.bar.ServerConfigFactory.initServerConfig()){

  System.out.println("bypassing com.foo.bar.ServerConfigFactory.initServerConfig");

  }

  让我用直白一点的方式为不了解AspectJ的读者介绍一下以上代码的含义吧:当运行时准备调用ServerConfigFactory.initServerConfig()方法,让它打印出一条信息,然后跳过该方法的执行并直接返回。听起来好像是某种hack,但它大大降低了开销。遗留系统中充斥着问题与谜团,与其打交道的每一时刻都得采取些策略。眼下,从客户满意度这点看来,对我来说最有意义的事情就是修复这个资源管理系统中的缺陷,并改善它的性能。其它方面的代码整理并不是我的当前目标,但我已记住了这个问题,我决定之后再回来处理ServerMain中的问题。

  接下来,在每一处HibernateUtil需要读取server.conf文件的地方,我都强制让它从database.properties中进行读取。

  String around():call(public String com.foo.bar.config.ServerConfig.getJDBCUrl()){

  // code omitted, reading from database.properties

  }

  String around():call(public String com.foo.bar.config.ServerConfig.getDBUser()){

  // code omitted, reading from database.properties

  }

  接下来的工作你大概能猜到了,如果使用临时方案会比较方便又显得自然,那就使用它。而如果有现成的mock对象可以使用,那么就重用它们。举例来说,TestServerMain.main()方法在某一时刻会产生如下错误:

  - Factory name: java:comp/env/hibernate/SessionFactory

  - JNDI InitialContext properties:{}

  - Could not bind factory to JNDI

  javax.naming.NoInitialContextException: Need to specify class name in environment

  or system property, or as an applet

  parameter, or in an application resource file: java.naming.factory.initial

  at javax.naming.spi.NamingManager.getInitialContext(NamingManager.java:645)

  at javax.naming.InitialContext.getDefaultInitCtx(InitialContext.java:288)

  这是由于JBoss命名服务未启动造成的,虽然我也可以使用相同的hack技术作为临时方案,但InitialContext是一个庞大的Javax接口,它包含了数量众多的方法,我可不想把每个方法都用hack方式给补完,那实在太冗长了。我很快发现Spring里已经包含了一个mock的SimpleNamingContext类了,那么就把它放到测试里去试试:

  SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();

  builder.bind(“java:comp/env/hibernate/SessionFactory”,sessionFactory);

  builder.activate();

  经过几次反复的修改后,我终于能够成功地运行TestServerMain.main()方法了。它比起ServerMain来说要简单许多,不仅mock了许多JBoss的服务,而且完全避免了集群管理的麻烦。

  创建构造块

  TestServerMain连接了某个真实的数据库,而遗留系统往往会在存储过程、甚至是触发器中隐藏了各种出人意料的逻辑。基于对系统整体情况的考虑,我认为在当前状况下试图理解数据库中的所有奥秘、并以此创建一个伪造的数据库是一个不明智的选择,因此我决定仍然在测试用例中访问真实的数据库。

  这些测试用例必须保证它们能够重复运行,以确保我对产品代码所做的任何小改动都能够通过测试。每一次运行,测试用例都会在数据库中创建资源与请求。与单元测试的习惯作法不同的是,有时你并不希望在每次运行之后对测试用例所创建的各种数据进行清理。我们目前为止所做的测试与重构练习更像是一次实地考查的探索——通过对遗留系统进行测试的方式来学习它的功能。为了确保一切功能都像预期一样工作,你也许需要在数据库中检查由测试用例所创建的数据,或者需要在运行时使用这些数据。这就意味着测试用例每一次运行时都要在数据库中创建新的唯一实体,以避免与其它测试用例相冲突。最好能编写一些实用工具类以方便地创建这些实体。

原文转自:http://www.infoq.com/cn/articles/refactoring-legacy-applications