徐昊 · TDD 项目实战 70 讲
徐昊
Thoughtworks 中国区 CTO
18159 人已学习
新⼈⾸单¥98
登录后,你可以任选4讲全文学习
课程目录
已完结/共 88 讲
实战项目二|RESTful开发框架:依赖注入容器 (24讲)
实战项目三|RESTful Web Services (44讲)
徐昊 · TDD 项目实战 70 讲
15
15
1.0x
00:00/05:14
登录|注册

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

讲述:徐昊大小:4.79M时长:05:14
你好,我是徐昊。今天我们来继续进行命令行参数解析的 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),并提取接口:
00:00 / 00:00
    1.0x
    • 2.0x
    • 1.5x
    • 1.25x
    • 1.0x
    • 0.75x
    • 0.5x
    网页全屏
    全屏
    00:00
    接着,再将修改后的方法内联回去(Inline Method)。经过这两步,我们引入了多态的接口,接下来就要消除分支了。由于我们无法扩展内建类型 Class 类,因此只能使用“抽象工厂”模式(Abstract Factory)的变体来替换分支:
    00:00 / 00:00
      1.0x
      • 2.0x
      • 1.5x
      • 1.25x
      • 1.0x
      • 0.75x
      • 0.5x
      网页全屏
      全屏
      00:00
      好了,我们已经消除了分支语句的坏味道。如果再看一下现在的代码,会发现还有另一个坏味道:代码重复(Duplication)。同样,这也是一个不小的重构操作。我们需要保持小步骤且稳定的节奏,逐步完成重构,而不是按照目标对代码进行重写:
      00:00 / 00:00
        1.0x
        • 2.0x
        • 1.5x
        • 1.25x
        • 1.0x
        • 0.75x
        • 0.5x
        网页全屏
        全屏
        00:00

        小结

        至此为止,我们就完成了对于代码的重构。回想我们写下的第一段生产代码:
        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 认为我们很难同时达到功能正确且结构良好(对,不是针对谁,在座的诸位…),因而通过红 / 绿 / 重构,也就是先功能后结构的方式,降低了达成这个目标的难度。
        下节课,我们将在这段代码的基础上完成后续功能的开发。我们将会看到,这次重构将会对任务列表产生什么影响。

        思考题

        在重构的时候,如果先消除重复代码,那么在重构步骤上会有什么不同?
        如果你在学习过程中还有什么问题或想法,欢迎加入读者交流群。最后,也欢迎把你学习这节课的代码与体会分享在留言区,我们下节课再见!
        确认放弃笔记?
        放弃后所记笔记将不保留。
        新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
        批量公开的笔记不会为你同步至部落
        公开
        同步至部落
        取消
        完成
        0/2000
        荧光笔
        直线
        曲线
        笔记
        复制
        AI
        • 深入了解
        • 翻译
          • 英语
          • 中文简体
          • 中文繁体
          • 法语
          • 德语
          • 日语
          • 韩语
          • 俄语
          • 西班牙语
          • 阿拉伯语
        • 解释
        • 总结

        徐昊在本文中进行了TDD演示,重点讲解了识别坏味道与代码重构的过程。文章首先介绍了命令行参数解析的需求,并展示了代码的进展。作者指出了代码中存在的坏味道,即面向对象误用和代码重复,并提出了利用多态替换条件分支的重构方法。通过抽取方法、提取接口和消除分支等步骤,作者成功消除了代码中的坏味道。文章强调了TDD的红/绿/重构循环的重要性,以及TDD对于降低编程过程中的恐惧的作用。最后,作者提出了一个思考题,引发读者对重构过程的思考。整体而言,本文通过实际案例生动地展示了TDD的应用和代码重构的重要性,对于想要提升编程技能的读者具有一定的借鉴意义。

        2022-03-1642人觉得很赞给文章提建议

        仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
        《徐昊 · TDD 项目实战 70 讲》
        新⼈⾸单¥98
        立即购买
        unpreview
        登录 后留言

        全部留言(27)

        • 最新
        • 精选
        • 🐑
          置顶
          大家好~我是TDD这门课的编辑辰洋~ 🎐我来友情提示一下下~ 01-04是视频演示,好对TDD有个感性的认识。 05-10是对演示的复盘,同时也会讲解TDD的相关知识点。比如测试的基本结构,TDD中的测试是啥~ 所以,如果你在01-04的操作卡壳了,可以从05开始学,看看5-10,这才是重点哇。看完再回头去看01-04~
          2022-03-22
          5
        • aoe
          置顶
          1. 跟着徐老师学习了Idea重构的强大功能 2. 本课学习笔记 https://www.wyyl1.com/post/19/02/ 3. 源码 https://github.com/wyyl1/geektime-tdd/tree/branch-02/ 4. 笔记摘要 优化思路 不同的实现提取出接口 实现接口 if else 分支使用接口替换 构造函数转工厂方法(因为工厂方法可以 inLine,构造函数不行) 尽可能使用接口 inLine 精简代码(不用跳转到方法看具体实现) 消除多余代码 代码和老师不一样的地方 区别:提前抽取了公共变量 String flag = “-” + option.value(); 理由: DRY 原则 此时徐昊老师的代码里有多处重复 感觉出现问题: 我的命名比较糟糕:SingleValueOptionParser 类中 parse(List arguments, String flag),参数和 Option 无关 因为将 Option 转换成了 String,后续多参数解析,可能会出问题
          2022-03-22
          1
        • 阿崔cxr
          置顶
          交一下第一章和第二章的作业 环境: nodejs 语言: typescript 暂时先把 happy path 搞定了 https://github.com/cuixiaorui/args

          作者回复: nice

          2022-03-19
          2
          1
        • 术子米德
          🤔☕️🤔☕️🤔 * 📖:极限编程提出,第一原则是“勇气(Courage)”,第一大敌叫“恐惧(Fear)”。 * 🤔:如果是代码新手,回忆一下我自己是代码新手的时候,看到这两个词,有种摸不着头脑的不明觉厉感。现在看到这两个词,尤其是“勇气”,脑子里第一浮现,就是自己写的代码,有没有勇气拿出来晒,有没有勇气持续去修改。自己以前写的代码,所谓功能正确,是指合并到一个大系统,整体上看起来满足需求。如果局部代码修改,就再得合并到大系统进行验证。这个动作不仅慢,而且不精准,因为无法准确判断哪些代码对应哪些操作。这就导致懒得去改进代码,也就慢慢失去改进自己代码的勇气。当代码在多个地方被使用,不敢改代码的勇气,会变成改代码的恐惧。只要有一次不小心,带来很多恶性评价或投案,这份恐惧就会与日俱增,至于改代码的勇气,早就荡然无存。 * 🤔:现在,我正在努力让测试代码和生产代码待在一起,只有让持续改进代码变得物理上阻力最小,才能留住持续改代码的勇气,更不会让恐惧滋生。今天看到Courage和Fear这两个词,忽然间给我内心更坚定的力量,要去把眼下正在推进的Mod[AutoUT/IT/ST]变成一项事业。只有将它成为自己的一项事业,才能让Courage持续加分,而Fear会自动减分,也不会在意别人无意间的戏虐。最终让写代码的人意识到,实际上,当自己保持住修改代码的Courage,就是在为自己的成就感做祭奠,最终在整个过程里体验到,一小步一小步改进的幸福感。

          作者回复: nice

          2022-04-05
          5
        • 爱吃彩虹糖的猫~
          交一下02课的作业,提交记录为:2022-07-16的commit记录 https://github.com/pengjunzhen/my-geektime-tdd/commits/master

          编辑回复: 很棒很棒!good work!

          2022-07-16
        • Gojustforfun
          Go演示git提交记录: https://github.com/longyue0521/TDD-In-Go/commits/args 这次我利用Github上提交记录按天分隔的特性使提交记录更好找. Commits on Mar 26, 2022 ~ Commits on Mar 25, 2022之间的内容与本篇文章对应. 采用baby step每步都有提交记录可以对比学习.如果觉得本项目对你有帮助,欢迎留言、star

          作者回复: nice

          2022-03-26
        • webmin
          先实现再优化,就好像同一时间只能带一顶着帽子出门一样,耐心按着步聚来,快与慢是相对的,一开始慢,之后才可以越来越快。 之前一直觉得Idea的重构功能太少,看了演示才知道其实是自己不会用,回想一下Idea怎么说也是经过全世界的千锤百炼,其中的重构功能是经过凝练的,这也从一个侧面反应出自己对重构的认知太过表面,功能通过组合可以适用很多场景。

          作者回复: 重构不需要太花哨的功能 有四五个就够用

          2022-03-22
        • Quintos
          个人 C# 版练习提交地址: https://github.com/dengyakui/GeekTDD/blob/master/GeekTDD/ArgsTest.cs

          作者回复: good

          2022-03-20
        • 百炼钢
          Refactor > Replace Constructor with Factory Method... ,这个只有正版IDEA才有此功能吧?

          作者回复: 在构造函数上按 就有了

          2022-03-20
          6
        • 冯俊晨
          对于和我一样不熟悉IntelliJ Idea的同学,会对于快捷键很迷惑。在Mac上,徐老师常用的快捷键是: 抽取变量:OPTION+COMMAND+V inline:OPTION+COMMAND+N 抽取方法:OPTION+COMMAND+M Generate:OPTION+N 万能通用:OPTION+ENTER
          2022-05-27
          12
        收起评论
        大纲
        固定大纲
        识别坏味道
        利用多态替换条件分支
        小结
        思考题
        显示
        设置
        留言
        27
        收藏
        8
        沉浸
        阅读
        分享
        手机端
        快捷键
        回顶部