徐昊 · TDD项目实战70讲

02|TDD演示(2):识别坏味道与代码重构

徐昊

Thoughtworks中国区CTO

你好,我是徐昊。今天我们来继续进行命令行参数解析的TDD演示。

首先让我们回顾一下题目与需求与代码进度。如前所述,题目源自Bob大叔的 Clean Code 第十四章:

我们中的大多数人都不得不时不时地解析一下命令行参数。如果我们没有一个方便的工具,那么我们就简单地处理一下传入main函数的字符串数组。有很多开源工具可以完成这个任务,但它们可能并不能完全满足我们的要求。所以我们再写一个吧。
 
传递给程序的参数由标志和值组成。标志应该是一个字符,前面有一个减号。每个标志都应该有零个或多个与之相关的值。例如:
 
-l -p 8080 -d /usr/logs
 
“l”(日志)没有相关的值,它是一个布尔标志,如果存在则为true,不存在则为false。“p”(端口)有一个整数值,“d”(目录)有一个字符串值。标志后面如果存在多个值,则该标志表示一个列表:
 
-g this is a list -d 1 2 -3 5
 
"g"表示一个字符串列表[“this”, “is”, “a”, “list”],“d"标志表示一个整数列表[1, 2, -3, 5]。
 
如果参数中没有指定某个标志,那么解析器应该指定一个默认值。例如,false代表布尔值,0代表数字,”"代表字符串,[]代表列表。如果给出的参数与模式不匹配,重要的是给出一个好的错误信息,准确地解释什么是错误的。
 
确保你的代码是可扩展的,即如何增加新的数值类型是直接和明显的。

识别坏味道

在通过5次红/绿循环之后,我们完成了第一块大的功能,可以处理多个参数并且支持布尔、整数和字符串类型的参数(当然,并不包含错误格式处理)。目前的代码看起来是这样的:

package geektime.tdd.args;
import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.List;


public class Args {
  public static <T> T parse(Class<T> optionsClass, String... args) {
    try {
      List<String> arguments = Arrays.asList(args);
      Constructor<?> constructor =
          optionsClass.getDeclaredConstructors()[0];
      Object[] values =
          Arrays.stream(constructor.getParameters()).map(it ->   
              parseOption(arguments, it)).toArray();
      return (T) constructor.newInstance(values);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  
  private static Object parseOption(List<String> arguments, Parameter
      parameter) {
    Object value = null;
    Option option = parameter.getAnnotation(Option.class);
    if (parameter.getType() == boolean.class) {
      value = arguments.contains("-" + option.value());
    }
    if (parameter.getType() == int.class) {
      int index = arguments.indexOf("-" + option.value());
      value = Integer.parseInt(arguments.get(index + 1));
    }
    if (parameter.getType() == String.class) {
      int index = arguments.indexOf("-" + option.value());
      value = arguments.get(index + 1);
    }
    return value;
  }
}

现在我们有两个选择:继续完成功能,或者开始重构。是否进入重构有两个先决条件,第一是测试都是绿的,也就是当前功能正常。第二是坏味道足够明显。

显然我们的测试都是绿的,而且到达了一个里程碑点,完成了一大块功能。同样,目前代码中存在明显的坏味道,就是这段代码:

 if (parameter.getType() == boolean.class) {
      value = arguments.contains("-" + option.value());
    }
    if (parameter.getType() == int.class) {
      int index = arguments.indexOf("-" + option.value());
      value = Integer.parseInt(arguments.get(index + 1));
    }
    if (parameter.getType() == String.class) {
      int index = arguments.indexOf("-" + option.value());
      value = arguments.get(index + 1);
    }

可以看到,这段代码中存在多个分支条件。而且可以预见,随着我们要支持的类型越来越多,比如double类型,那么我们还需要引入更多类似的结构。

这是一个明显的面向对象误用的坏味道——分支语句(Switch Statements、Object-Oriented Abusers)。而我们可以利用重构手法“利用多态替换条件分支”(Replacing Conditional with Polymorphism)对其进行重构。

利用多态替换条件分支

需要注意的是,“利用多态替换条件分支”是一个相当大的重构,我们需要一系列的步骤才能完成这个重构。这期间,我们需要保持小步骤且稳定的节奏,逐步完成重构,而不是按照目标对代码进行重写。所以在观看下面的视频时,请留心数一下,在整个重构过程中,我运行了多少次测试。

首先,将需要重构的部分抽取方法(Extract Method),并提取接口:

接着,再将修改后的方法内联回去(Inline Method)。经过这两步,我们引入了多态的接口,接下来就要消除分支了。由于我们无法扩展内建类型Class类,因此只能使用“抽象工厂”模式(Abstract Factory)的变体来替换分支:

好了,我们已经消除了分支语句的坏味道。如果再看一下现在的代码,会发现还有另一个坏味道:代码重复(Duplication)。同样,这也是一个不小的重构操作。我们需要保持小步骤且稳定的节奏,逐步完成重构,而不是按照目标对代码进行重写:

小结

至此为止,我们就完成了对于代码的重构。回想我们写下的第一段生产代码:

Constructor<?> constructor = optionClass.getDeclaredConstructors()[0];

try {
  return (T) constructor.newInstance(true);
} catch(Exception e) {
  throw new RuntimeException(e);
}

在这个过程中,我们经历了5次红/绿循环,完成了主要功能。同时,也累计了代码坏味道。然后我们通过重构,消除了代码坏味道。在保持功能不变的前提下,得到了结构更好的代码。我估计你大概率是想不到,40分钟以后,我们会得到目前的代码结构。

TDD的红/绿/重构循环,分离了关注点。在红/绿阶段,我们不关心代码结构,只关注功能的累积。而在重构的过程中,因为测试的存在,我们可以时刻检查功能是否依旧正确,同时将关注点转移到“怎么让代码变得更好”上去。

说句题外话,Kent Beck作为极限编程(Exetreme Programming)的创始人,将勇气(Courage)作为极限编程的第一原则,提出编程的第一大敌是恐惧(Fear),实在是有非凡的洞见。同时,他也花了极大的篇幅,说明为什么TDD可以让我们免于恐惧:重构使得我们在实现功能时,不恐惧于烂代码;测试使得我们在重构时,不恐惧于功能破坏。

某种意义上说,TDD认为我们很难同时达到功能正确且结构良好(对,不是针对谁,在座的诸位…),因而通过红/绿/重构,也就是先功能后结构的方式,降低了达成这个目标的难度。

下节课,我们将在这段代码的基础上完成后续功能的开发。我们将会看到,这次重构将会对任务列表产生什么影响。

思考题

在重构的时候,如果先消除重复代码,那么在重构步骤上会有什么不同?

如果你在学习过程中还有什么问题或想法,欢迎加入读者交流群。最后,也欢迎把你学习这节课的代码与体会分享在留言区,我们下节课再见!

教程推荐

Matplotlib在线教程

MATLAB在线教程

Django在线教程

HTML在线教程

Nginx在线教程

TensorFlow2 快速入门在线教程

随机推荐

和府捞面浓汤鲜煮面4袋速食面番茄牛肉面条选购技巧有哪些?体验...

朗适RS100功能是否出色?深度评测教你怎么选?

狮王小狮王儿童氟防蛀牙膏 20g性价比如何?真实评测体验曝光!

狮王小狮王儿童氟防蛀牙膏 20g选购哪种好?网友评测点评分享?

雀巢超启能恩奶粉3段760g*4罐点评怎么样?评测分享?

佳沃云南蓝莓14mm 12盒原箱生鲜实用性高,购买推荐吗?产品功能...