上周在公司技术论坛上现场演示了怎么用TDD实现分解质因数的算法,感觉给大家留下了一个“TDD不需要怎么思考就把问题解决了”的印象,当场就有同学“约战”要再选一道题,于是就选了我们公司的一道笔试题:
Given a string containing only three types of characters: '(', ')' and ' * ', write a function to check whether this string is valid. We define the validity of a string by these rules:
Any left parenthesis '(' must have a corresponding right parenthesis ')'.
Any right parenthesis ')' must have a corresponding left parenthesis '('.
Left parenthesis '(' must go before the corresponding right parenthesis ')'.
' * ' could be treated as a single right parenthesis ')' or a single left parenthesis '(' or an empty string.
An empty string is also valid.
e.g.
Input: "( )" Output: True
Input: ") (" Output: False
Input: "( ) )" Output: False
Input: "(( * )" Output: True
Input: "(( * )()" Output: True
Input: "(( * ))))" Output: False
Input: "(( * * * ( ( * )" Output: True
Input: "" Output: True
TDD要求我们思考的是最容易通过的测试是什么,比如这道题,大部分同学都会选择空字符串作为第一个测试用例。
assertTrue(isValid(""))
实现代码也很简单,
return input.equals("");
很多同学对TDD的理解就到这里了,其实这只是TDD的第一步,其实TDD的难点在于思考和设计测试的顺序,这个顺序要遵守从易到难得原则,顺序不对,很可能就进入不了“小步快跑”的节奏,以这道题为例,我会先思考并设计出下面的测试顺序:
"" -> "(" -> ")" -> "( )" -> "( (" -> "( ) ) )" -> "( ) ( )" -> "( ( ) )" -> ...
可以看出我是根据输入字符串的长度按从小到大的顺序安排测试顺序的,而且故意把通配符" * "暂时扔到了一边,因为我觉得引入通配符会引入实现复杂度,所以我把它的顺序尽量往后放。
除了测试顺序需要思考,另外一个需要思考的是怎么能一方面能以最小的代价让新的测试通过,另一方面又能使你的代码更接近问题的本质?
假如当你的当前代码是:
if ("(".equals(input)) return false;
怎么让下面的测试快速通过?
assertFalse(isValid(")"))
假设你是这么写的:
if ("(".equals(input) || ")".equals(input)) return false;
虽然测试通过了,代码也很简单,但是请思考一下,这样实现以后你的代码更智能了吗,更接近问题的本质了吗?答案是否定的。那有没有既简单又智能的实现呢?我会选择这样去实现:
if (input.length % 2 != 0) return false;
再比如你的代码演化到下面时:
if (input.equals("")) return true;
if (input.length % 2 != 0) return false;
if (isMatch(input.charAt(0), input.charAt(1))
return true;
return false;
当你要通过下面这个测试时,你会怎么写实现代码?
assertFalse(isValid("( ) ) )"))
这时候TDD并不能代替你思考,思考还是逃不掉的,分析新的测试,我们可以看出仅仅一个括号配对是不够的,剩余部分也应该配对,所以代码可以写成:
if (input.equals("")) return true;
if(input.length%2!=0) return false;
if (isMatch(input.charAt(0), input.charAt(1))
return isValid(removeCharAt(input, 0, 1));
return false;
总结一下,TDD只是一个引导你思考的工具,并不能替代你的思考,使用TDD反而需要你更多地思考测试的顺序,更多地思考你的代码是不是在逐步接近问题的本质。最后留给大家思考接下来怎么让下面这个测试通过以及什么时候引入通配符。
assertTrue("( ( ) )")
网友评论