04 TDD演示(4):实现对于列表参数的支持
你好,我是徐昊。今天我们来继续进行命令行参数解析的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代表数字,”"代表字符串,[]代表列表。如果给出的参数与模式不匹配,重要的是给出一个好的错误信息,准确地解释什么是错误的。
确保你的代码是可扩展的,即如何增加新的数值类型是直接和明显的。
截至目前,我们的代码支持三种类型的参数解析,分别是布尔型、整型和字符串类型。接下来,我们来实现对于列表参数的支持。在开始之前,我们首先要看一看是否存在坏味道,是否需要重构。
不易察觉的坏味道
对我来说呢,在当前的代码中存在一个不易察觉的坏味道,意图也不直观,主要存在于SingleValuedOptionParser类的parse方法中:
if (index + 1 == arguments.size() ||
arguments.get(index + 1).startsWith("-")) throw new InsufficientArgumentException(option.value());
if (index + 2 < arguments.size() &&
!arguments.get(index + 2).startsWith("-")) throw new TooManyArgumentsException(option.value());
如果我们静下心来仔细推敲,不难发现,第一个if语句表示的是参数不足的情况,分别为:当前参数到达列表末尾(-p的情况);紧紧跟随另一个参数(-p -l的情况)。
第二个if语句则表示,当前参数后至少还存在两个数值,且第二个不是另一个参数(-p 8080 8081,而不是-p 8080 -l的情况),那么参数给多了。
一般这种情况下,我们可能会选择添加代码注释的方式。不过更推荐的方式是,通过抽取方法,让方法名成为注释。或者,换一种更容易理解的方法来实现同样的功能:
重构后的代码让我们的意图变得非常直观,获取当前参数的值,而且我们明确希望它的长度为1:
List<String> values = valuesFrom(arguments, index);
if (values.size() > 1) throw new InsufficientArgumentException(option.value());
if (values.size() < 1) throw new TooManyArgumentsException
(option.value());
完成重构之后,我们又会发现另一个坏味道,BooleanOptionParser和SingleValuedOptionParser之间存在隐含的重复的代码:
在消除了重复之后,让我们重新整理一下代码结构:
好,现在让我们正式进入列表参数解析的开发。
列表参数解析
现在我们对于如何实现参数解析已经有了非常清晰的认识。在目前的代码结构中,如果需要增加不同类型的单值型参数,那么我们只需要修改Args类中的类型注册表,提供默认值以及解析函数即可:
private static Map<Class<?>, OptionParser> PARSERS = Map.of(
boolean.class, bool(),
int.class, unary(0, Integer::parseInt),
String.class, unary("", String::valueOf));
而如果需要支持除布尔或者单值型参数,则需要实现OptionParser接口:
更具体来说,在实现OptionParser接口时,可以利用OptionParsers类中提供的支撑方法(values、parseValue等)。最后,在OptionParsers上增加工厂方法。
在回顾了这些信息之后,我们可以对列表参数进行任务分解了。从题目要求的功能上看,我们需要实现:
然后,我们需要将其分解成一组更小的任务:
好,现在让我们进入红/绿/重构循环:
以及最后对于代码的清理与重构:
小结
至此,我们使用TDD的方法完成了参数解析的功能。我觉得你至少应该感受到了TDD这三个特点。
第一是,将要完成的功能分解成一系列任务,再将任务转化为测试,以测试体现研发进度,将整个开发过程变成有序的流程,以减少无效劳动。
第二是,在修改代码的时候,随时执行测试以验证功能,及时发现错误,降低发现、定位错误的成本,降低修改错误的难度。
第三是,时刻感受到认知的提升,增强自信降低恐惧。在针对列表参数使用任务分解法时,你明显可以感觉到,我们无论是对需求的把握性,还是对最终实现的可预见性,都有了大幅度的提升。甚至,如果更进一步要求,我们可以较有把握地评估(误差在15%以内)实现列表参数解析需要多长时间。这就是我们认知提升的具体体现。
我将这样的工作状态称为“职业程序工作状态”:有序、可控、自信。
很多同学可能是第一次目睹TDD在实战中是如何工作的,心中肯定充满了疑问。而另一些有过TDD实践的同学,也可能会发现我所采用的方法和步骤与你的方式有很大不同,这也是很正常的。在接下来的六节课中,我将带你复盘整个流程,并对其中涉及到的技巧和方法进行深入讨论。下节课,让我们正式开始学习TDD吧!
思考题
请自己尝试使用TDD从头实现命令行参数解析的功能。
如果你在学习过程中还有什么问题或想法,欢迎加入读者交流群。最后,也欢迎把你学习这节课的代码与体会分享在留言区,我们下节课再见!
- 🐑 👍(4) 💬(0)
大家好~我是TDD这门课的编辑辰洋~ 🎐我来友情提示一下下~ 01-04是视频演示,好对TDD有个感性的认识。 05-10是对演示的复盘,同时也会讲解TDD的相关知识点。比如测试的基本结构,TDD中的测试是啥~ 所以,如果你在01-04的操作卡壳了,可以从05开始学,看看5-10,这才是重点哇。看完再回头去看01-04~
2022-03-22 - webmin 👍(5) 💬(1)
通过观察最初列出的TODO List与最后完成的代码,感觉两者更像是接口定义与实现的关系,TODO List是列出了想要的功能定义,功能的实现是可以有多种的方式和不同的质量的,TDD是帮助我们从可用代码进化到优质代码的一种优秀的工程实践方法。 到达目标(TODO List)的路很多条,在没有上帝视角的情况下,唯有通过实践才能得知在当前条件下哪个路径是最佳的,TDD可以帮助减小改进的成本,固化测试知识,每进行一次回归测试收益就增加一次。
2022-03-27 - hippie 👍(1) 💬(1)
个人 C# 版本代码最终示例:https://github.com/hippieZhou/geektime.tdd
2022-05-15 - aoe 👍(1) 💬(1)
感受到了极限编程的气息! 学习笔记:http://wyyl1.com/post/19/04/
2022-03-27 - davix 👍(0) 💬(1)
//TODO: -d a throw exception 這個case的含義不能理解,請教老師
2022-05-03 - Frode 👍(0) 💬(1)
照葫芦画瓢写了一版本:https://gitee.com/frode_success/tdd.git 这周的目标把本周更新的三节理论课学习完成后,再独立写一遍
2022-03-28 - Leo 👍(0) 💬(1)
交作业,请多多指教 https://github.com/JTR354/tdd-practice
2022-03-19 - Frode 👍(23) 💬(1)
我好像是个假的java程序员,为什么能这么6
2022-03-22 - 阿崔cxr 👍(8) 💬(0)
文中几个点 都是当初学习 tdd 的时候 纠结半天的点 比如 1. 一开始从接口出发 那么这么大的步骤如何驱动出来 2. 重构出来多个类之后,应该如何选择测试哪个 (是继续测试大的,或者测试小的功能) 当初自己思考了很久, 这几篇文章就可以明白了。 这节约了太多时间了 [苦涩]
2022-03-17 - 🌊 👍(1) 💬(0)
高手之上的高高手
2023-09-15 - Y 👍(1) 💬(0)
佩服,我怀疑自己是个假程序员😂
2022-10-01 - Geek_wip5z8 👍(1) 💬(0)
完整跟着老师敲了一遍,学到了不少重构的skills,太赞了!!!
2022-03-26 - sky 👍(1) 💬(0)
看到了差距,不仅仅是折服。
2022-03-19 - 王智 👍(0) 💬(0)
看到老师的代码,我发现原来我并不会写代码,路还很长,还得练习呀
2024-03-20 - Geek_b6d0fd 👍(0) 💬(0)
我发现一个疑似bug的地方,我需要增加一个测试验证,明天试一下,当多字符串参数中存在以 - 开头的字符,例如 assertEquals(new String[]{"-a", "-b"}, value);
2023-07-19