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

发表于:2013-12-20来源:InfoQ作者:Chen Ping点击数: 标签:重构
SummaryOfEstimatedWorkloadForRequestType, {30240=[ ActualWorkloadForReqeustWithId_30240, EstimatedWorkloadForRequestWithId_30240], 30241=[ ActualWorkloadForReqeustWithId_30241, EstimatedWorkloadForReq

  SummaryOfEstimatedWorkloadForRequestType,

  {30240=>[

  ActualWorkloadForReqeustWithId_30240,

  EstimatedWorkloadForRequestWithId_30240],

  30241=>[

  ActualWorkloadForReqeustWithId_30241,

  EstimatedWorkloadForRequestWithId_30241]

  }

  SummaryOfActualWorkloadForTicketType,

  SummaryOfEstimatedWorkloadForTicketType,

  {20000=>[

  ActualWorkloadForTicketWithId_2000,

  EstimatedWorkloadForTicketWithId_2000],

  }

  ]

  }

  这个糟糕的数据结构使得数据的编码与解码逻辑在直观上非常冗长乏味,可读性很差。

  集成测试

  希望我已经让你认识到这段代码确实是非常复杂的。如果在开始重构之前让我首先解开这团乱麻,然后理解每一行代码的意图,那我非疯了不可。为了保持我的心智健全,我决定采用一种自顶向下的方式来理解代码逻辑。也就是说,我决定首先尝试一下这个系统的功能,进行一些调试,以了解系统的整体情况,而不是一上来就直接阅读代码,并试图从中推断出代码的逻辑。

  我所使用的方法与编写测试代码完全相同,传统的方法是编写小段的测试代码以验证每一段代码路径,如果每一段测试都通过,那么当所有的代码路径组织在一起之后,方法能够按照预期方式工作的机会就很高了。但这种传统方式在这里行不通, ResourceBreakdownService简直就是一个“上帝类”,如果我仅凭着对系统整体情况的一些了解就对这个类进行分解,很可能会造成很多问题 – 在遗留系统的每个角落里都有可能隐藏着众多不为人知的秘密。

  我编写了以下这个简单的测试,它反映了我对整个系统的理解:

  public void testResourceBreakdown(){

  Resource resource=createResource();

  List requests=createRequests();

  assignRequestToResource(resource, requests);

  List tickets=createTickets();

  assignTicketToResource(resource, tickets);

  Map result=new ResourceBreakdownService().search(resource);

  verifyResult(result,resource,requests,tickets);

  }

  注意一下verifyResult()这个方法,我首先循环式地将result的内容打印出来,以找出其中的结构,随后verifyResult()方法根据这个结构对结果进行验证,确保其中包含了正确的数据:

  private void verifyResult(Map result, Resource rsc, List requests,

  List tickets){

  assertTrue(result.containsKey(rsc.getId()));

  // in this simple test case, actual workload is empty

  UtilizationBean emptyActualLoad=createDummyWorkload();

  List resourceWorkLoad=result.get(rsc.getId());

  UtilizationBean scheduleWorkload=calculateWorkload(rsc,requests);

  assertEquals(emptyActualLoad,resourceWorkLoad.get(0));

  assertEquals(scheduleWorkload,resourceWorkLoad.get(1));

  Map requestDetailWorkload = (Map)resourceWorkLoad.get(3);

  for (Request request : requests) {

  assertTrue(requestDetailWorkload.containsKey(request.getId());

  UtilizationBean scheduleWorkload0=calculateWorkload(rsc,request);

  assertEquals(emptyActualLoad,requestDetailWorkload.get(request.getId()).get(0));

  assertEquals(scheduleWorkload0,requestDetailWorkload.get(request.getId()).get(1));

  }

  // omit code to check tickets

  ...

  }

  用临时方案绕过障碍

  以上测试用例看起来简单,但实际却隐含了许多复杂性。首先,ResourceBreakdownService().search方法与运行时紧密相关,它需要访问数据库、其它服务,或许还有些不为人知的依赖项。而且和许多遗留系统一样,这个系统也没有建立任何单元测试的架构。为了访问运行时服务,唯一的选择就是启动整个系统,这不仅造成巨大的开销,而且也带来了很大的不便。

  ServerMain类启动了整个系统的服务端功能,这个类也是个老古董了,你完全可以从中观察到它的进化过程。这个系统的编写时间已经超过10年了,当时还没有Spring、Hibernate这些东西,JBoss和Tomcat也才刚刚冒头。因此那些勇敢的先驱们不得不手工打造了许多工具,他们创建了一个自制的集群、一个缓存服务、一个连接池以及其它许多东西。之后他们在某种程度上引入了JBoss和Tomcat(但不幸的是他们仍然保留了那些手工艺品,导致了现在的代码中存在着两种事务管理机制以及三种连接池)。

  我决定将ServerMain复制到TestServerMain类中,但运行TestServerMain.main()方法产生了以下失败信息:

  org.springframework.beans.factory.BeanInitializationException: Could not load

  properties; nested exception is

  java.io.FileNotFoundException: class path resource [database.properties] cannot

  be opened because it does not exist

  at

  org.springframework.beans.factory.config.PropertyResourceConfigurer.

  postProcessBeanFactory(PropertyResourceConfigurer.java:78)

  好吧,它还挺灵活!我随意拿了个database.properties文件,把它放到测试类的文件夹中并再次运行测试。但这一次程序又抛出了下面的异常:

  java.io.FileNotFoundException: .\server.conf (The system cannot find the file specified)

  at java.io.FileInputStream.open(Native Method)

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