读《驯服烂代码——在编程操练中悟道》

读《驯服烂代码——在编程操练中悟道》

  • 读驯服烂代码在编程操练中悟道
    • 第2章 按图索骥地编写代码
    • 第4章 调试一下
    • 第5章 用TDD重做编程操练题目
    • 第6章 消除假数据所带来的重复代码
    • 第8章 嗅出代码腐臭和新的测试点
    • 第9章 测试后行 vs 测试先行
      • TDD开发方法具有的优势
    • 第10章 何谓烂代码
      • 烂代码的定义
    • 第一个项目实例
    • 第11章 记录所闻到的腐臭
    • 第12章 用测试描绘用户意图
    • 第13章 分而治之釜底抽薪
    • 第14章 分而治之抛砖引玉
    • 第15章 打扫战场
    • 第二个项目实例
    • 第16章 分而测之编写Stub及提取接口
      • 术语定义
      • 技能
    • 第三个项目实例
    • 第17章 分而测之编写Mock及子类化并覆盖方法
    • 第四个项目实例
    • 第18章 真正的单元测试
    • 第五个项目实例
    • 第19章 驯服烂代码的步骤IePpTr
      • IePpTr的定义
      • IePpTr的步骤
      • 章节总结
    • 第20章 习惯出自专注长期和用心的结对操练

第2章 按图索骥地编写代码

  1. 在IDEA中把光标定位到那些红色的有编译错误的代码上,然后按快捷键Alt+Enter能快速帮助我们生成所需要的代码。

  2. 如果能做到当有少量代码改动时就频繁地把代码提交到本地代码库而不管其是否通过编译,且每次提交都能填写有关此次代码改动的、意图明确的Commit Message,那么这种每次少量且意图描述清晰的代码提交,一方面增加了将来阅读代码变动的可读性,另一方面当代码写错需要回退时也能有助于做到更精细的回退。

第4章 调试一下

  1. 测试后行的开发方法所表现出现的问题包括:文档经常与代码缺乏同步造成理解偏差,写main()方法进行的测试无法让计算机自动判断软件行为是否符合预期导致效率低下,问题产生后没能立即发现耽误修复时间,照搬设计模式导致设计出不必要的抽象和编写出从未被调用的方法造成时间浪费,程序调试过程无法让计算机来代替并自动化并自动化地反复使用导致代码维护时间剧增。

  2. 用测试后行的开发方法所开发出来的代码,会使与代码相关的所有人,在对代码的行为理解、问题感知和质量维护方面都反馈迟缓,其后果就是浪费这些人的时间、精力和金钱。

第5章 用TDD重做编程操练题目

  1. 把以前所提到的带有强制、专制和持久色彩的“需求”一词,换成了“产品特性”这种基于沟通的字眼,能够让产品特性在频繁沟通的保证下更加具有价值。

  2. 把测试方法的名字写得很长,并在单词之间加上空格,使得测试方法名成为易于阅读的文档。

  3. 一个测试通常包括Arrange、Act和Assert这3部分,分别表示测试准备、调用待测方法和验证结果是否符合预期。

  4. 在一个测试中先写出最后的Assert部分,然后推到出Act和Arrange这两部分代码,会更加符合TDD的测试驱动风格,营造出分形的和谐气氛。

  5. 没有适当的面向对象的设计,就无法写出合理的意图代码。所以在测试中编写意图代码就能促进在写测试代码时做出适当的设计。

  6. 在测试中写好了所有的意图代码后,由于生产代码还未编写,势必有许多编译错误,而这种用修复意图代码编译错误的方法来驱动出生产代码的做法,能够让生产代码的开发过程更加具有方向性。

  7. 在逐个解决上述编译错误的过程中,只写能让编译通过的、尽量少的代码,比如空类或空方法,而把编写其具体实现的内容留到后面运行测试出错时再写,同样能够让生产代码的开发过程更加具有方向性,且有助于编写出不多不少并能让测试运行通过的代码,避免了时间的浪费。

  8. 用直接返回假数据的方法,让测试尽快运行通过,这样能够快速结束当前编写测试的环节,进入下面的重构环节,并能进一步指出重构的方向。

第6章 消除假数据所带来的重复代码

  1. 把在编程中随时发现的要处理的问题,在代码中相应的位置写成TODO,正在处理的TODO标记为TODO-working-on,并在将来使用快捷键Alt+6来查看,CTRL++来展开TODO,能让随时发现的问题不致中断现有的工作,并且不会遗忘任何发现要做的事情。

第8章 嗅出代码“腐臭”和新的测试点

  1. 每次重构代码后都随时运行测试,以检查是否破坏了原有代码行为。

  2. 是否选择将测试代码中的重复代码移动到测试类中以@Before标注的方法中以消除重复,也因人而异。因为有人认为这些重复的代码在每个测试中都是上下文的一部分,如果被提取到@Before中,会造成阅读测试代码时频繁地跳跃,产生不便,所以这部分人偏向于不提取测试中的重复代码。

第9章 测试后行 vs 测试先行

TDD开发方法具有的优势:

  1. 把代码本身当成代码编写的沟通基础,令代码成为可以运行的文档,会让那些代码即文档的读者——包括程序员、测试工程师、产品专家等——在理解代码行为方面反馈迅速。

  2. 把测试的期望值写成Assert语句告诉计算机,使其能够代替人脑来判断结果是否符合预期,会让程序员在感知代码问题方面反馈迅速。

  3. 先写测试并频繁运行测试以立即发现错误,会让程序员、测试工程师和开发经理等所有与代码相关的人在感知代码问题方面反馈迅速。

  4. 专注在用“最少量”的代码让编译和测试运行通过,接着治理代码“腐臭”,通过计算机自动且反复运行的测试,发现那些被限定在一个个粒度很小的测试之内的bug,都会让程序员在维护代码质量方面反馈迅速。

第10章 何谓“烂代码”

烂代码的定义

  1. 能够运行。

  2. 需要修改其中的bug或在其中增加新功能。

  3. 对于代码的编写者、测试者和维护者来说反馈迟缓。

第一个项目实例

PS:码农酒店GitHub,该代码是驯服烂代码中的第一个实例。

第11章 记录所闻到的“腐臭”

  1. 所有程序员,不管是高手还是新手,每天都会面对烂代码的“腐臭”。

  2. 在对代码重构之前,必须针对要重构的代码编写测试。没有测试保护的代码修改不叫重构,而只能叫“裸奔”。

  3. 阅读代码时,可以把代码的公共方法看成图书中的章节,并将其作为指引,来阅读代码。

第12章 用测试描绘用户意图

  1. 测试的编写应该面向相对稳定的用户意图这个抽象,而不是面向相对易变的已有代码的具体实现。

  2. 首先编写表现用户意图的测试,并运行通过,来固化软件行为。然后在其保护下按在代码中出现的先后顺序来处理与代码“腐臭”相关的TODO。

  3. 在重构过程中,只要有可能运行测试,就尽量频繁地运行测试,以保证重构不会破坏已被测试所固化的软件行为。

  4. 在重构过程中需要修改那些已被客户端所使用的公共接口时,一定要谨慎,可以尽量往后放一放,以便有时间来确信这种改动对客户端的影响是否在可控范围内。

  5. 对于工作量较大的TODO,可以标记为later以便以后处理。

  6. 重复代码可以通过方法提取来解决。

  7. 去除重复代码的工作要尽早进行,否则随着时间的推移,重复代码会发生一些变化,与原来代码不一致,从而增大了消除重复代码的难度。

  8. 要消除重复代码,必须先把两段或多段重复代码恢复成相互之间完全相同的状态。

  9. 把用于日志记录的System.out.println()方法替换为logger.info()方法,并保存到日志文件中,便于阅读和分享。

第13章 分而治之——釜底抽薪

  1. 在把一个大类分解为若干小类前,要先消除重复代码这样的代码“腐臭”;

  2. 对于总是聚在一起的那些成员变量,往往可以把它们提取到一个新类中。

  3. 重构时替换代码的方法有两种,一种是直接用新代码替换旧代码的“釜底抽薪”法,另一种是照着旧代码这块“砖”编写新代码这块“玉”的“抛砖引玉”法。前者适用于简单的重构,后者适用于复杂的重构。

  4. 可以利用根据意图修改代码所产生的编译错误的指引,在“釜底抽薪”的重构方法中进行新旧代码的替换。

  5. 对于在重构过程中新识别出来的代码“腐臭”,可以随时添加新的TODO来记录,,待时机成熟时再处理。

  6. 对于新分解出来的类,要编写用户意图测试。

  7. 一个大TODO可以拆解为若干小TODO来完成。

  8. 编写代码主要是给人阅读的,需要确保代码在传递信息方面的语法和语义正确,比如要保证日志信息的语法正确。

第14章 分而治之——抛砖引玉

  1. 使用+=和-=可以消除重复代码。

  2. 应该本着高内聚和低耦合的原则在类之间移动成员方法。

  3. 使用“抛砖引玉”的重构方法能够最大限度地让测试频繁得到运行。

  4. 将魔法数提取成常量重命名,以增强其可读性、可维护性,并能消除重复代码。

  5. 使用“抛砖引玉”的重构方法时,若原有代码出现在if条件中,则其可以与其功能等价的意图代码做逻辑“或”运算而出现在同一个if条件中,这样就不会在代码运行时影响原有代码的功能。

第15章 打扫战场

  1. 用要保存下来的成员变量逐步取代要被替换的成员变量,最后再清除后者的定义的做法,来消除两个成员变量之间在逻辑上的重复。

  2. 将不易读的条件判断或返回值提取、命名成易读的解释性变量。

  3. 当对生产代码的行为理解不准时,编写出的测试会运行失败,此时需要根据用户意图和生产代码的实际行为调整测试代码,来达到二者的平衡。

  4. 当TODO不是很多且手上也没有正在处理的任务时,可以一发现代码“腐臭”就随时修改,不必再写TODO。

  5. 确保函数中的语句都处于同一抽象层次上。否则就使用提取方法的做法,来确保提取出来的每个方法中的语句都在同一抽象层次上。

  6. 对于未被使用的方法,可以大胆删除,写好Commit Message,等以后需要时再从版本管理系统中找出来也不迟。

  7. 可以使用卫语句来替代嵌套的条件表达式。

  8. 将多次使用的字符串提取成常量以增强可维护性,并能消除重复代码。

  9. 将包含多种用户意图的大的测试类分解为只包含一种用户意图的小的测试类。

  10. 当一个大类被分解为多个小类后,就可以把原先分布在大类中各处与小类相关的逻辑,按照小类的职责转移到相应的小类中。

  11. 每一次重构都可能让以前运行通过的测试运行失败。此时要频繁地运行测试,发现失败就立即解决,以缩小范围,降低查找问题的难度。

  12. 当实现新特性时,有时新特性的用户意图会与原有特性的用户意图有冲突,此时要根据新特性的用户意图更改原有的测试代码,进而更改生产代码,从而实现新特性的用户意图。

第二个项目实例

PS:骰子游戏GitHub,该代码是驯服烂代码中的第二个实例。

第16章 分而测之——编写Stub及提取接口

术语定义

  1. SUT(System Under Test):被测系统。

  2. DOC(Depended-On Component):被测系统所依赖的组件。

  3. Stub:桩模块,与依赖组件拥有相似却又容易控制的行为,提供可控间接输入,供被测系统使用,一般是与依赖组件实现同一接口。

  4. Mock:虚拟对象,与依赖组件拥有相似却又容易控制的行为,提供可控间接输入,还能验证间接输出,一般是继承依赖组件,并覆盖相应方法。

  5. Test Double:测试替身,Stub和Mock都属于Test Double。

技能

  1. 代码虽然易读,但由于没有测试保护,使其对于代码维护者来说反馈慢,所以这样的代码还是属于烂代码。

  2. 当SUT所依赖的DOC很难在测试中进行控制时,可以根据DOC提取出一个接口,然后让SUT不在针对DOC,而是针对这个接口编程,并根据这个接口编写易于控制的Test Double,来替代那个难以控制的DOC,从而令SUT把Test Double当成那个接口来看待,进而对SUT进行测试,来解决DOC在测试时难以控制的问题。

  3. Stub和Mock都属于Test Double。前者的作用是让测试能够控制SUT的间接输入,以便于测试能够强制SUT进入正常情况下很难进入的运行路径中;而后者的作用是让测试能够验证SUT的间接输出。

  4. 有关测试TODO应该针对抽象的用户意图来编写,而不要针对具体的编程实现逻辑来编写,以便让测试变得强壮而不是变得脆弱。

  5. 使用像Isensor这样的命名接口的方式并不理想,比较好的方法是用Google翻译来寻找一个类名的英文近义词来为该类接口命名。

第三个项目实例

PS:轮胎气压监测系统GitHub,该代码是驯服烂代码中的第三个实例。

第17章 分而测之——编写Mock及子类化并覆盖方法

  1. 一个在面向对象编程语言中所定义的具体的类,虽然在形式上不是一个接口,但如果该类有一个符合里氏替换原则的子类,那么在子类眼中,父类就可以被当成接口来使用。

  2. Mock除了完成Stub所做的为SUT在测试中的运行提供间接输入外,还要额外做验证SUT在测试中的间接输出的事情,所以Mock类中一般都有verify()这样的验证方法。

  3. 使用先编写意图代码,然后通过频繁运行测试,编写最少量的代码(如能让编译通过的尚未实现的空方法,和能让测试运行通过的上述空方法的最少量的实现代码)以修复编译和测试运行错误,来驱动出生产代码的开发方式,既能让我们在编写意图代码时进行适当的设计,并能编写最少量的能让测试运行通过的生产代码,以减少浪费,又能让我们逐步熟悉从各种编译和测试运行的错误信息中发现导致错误的原因,从而不再惧怕看到错误信息,并把这些信息当成指路的向导,避免盲目地编写生产代码。

  4. 与手工编写Mock相比,用Mockito框架编写Mock有以下好处,不必编写额外的Mock类,减少工作量;凭借Mockito框架的诸如mock()、when()和verify()这些方法的精心设计,能够让Mock代码可读性更高。

  5. 与用Mockito框架编写Mock相比,手工编写Mock也有下列好处,能够完全控制Mock的编写过程;熟悉手工编写Mock的过程后,有利于理解Mock框架的使用方法。

第四个项目实例

PS:自动取号系统GitHub,该代码是驯服烂代码中的第四个实例。

第18章 真正的单元测试

  1. 运行速度慢的不是单元测试。与数据库、网络、文件系统和配置文件有交互的测试不是单元测试。

  2. 单元测试要测的仅仅是SUT里面的软件行为,而不是测试SUT与诸如数据库、网络和文件系统这些DOC之间是否能正常交互。

  3. 要把SUT与诸如数据库、网络、文件系统这些的DOC之间进行交互的集成测试,转变为针对SUT的单元测试,可以先找到DOC的接口,让SUT针对这个接口编程,并且编写一个运行起来经济快捷的Test Double来实现这个接口,并注入SUT中,让SUT把这个Test Double当成那个接口来使用。这样就能把依赖于DOC的集成测试,转换为依赖于实现接口的Test Double的单元测试。

  4. 用外覆类包装了针对第三方类库的使用,即在第三方类库的外面外覆一个类,然后让生产代码针对这个外覆类的接口编程,而让第三方类库在这个外覆类中实现这些接口,以便于将来更换第三方类库。

第五个项目实例

PS:Unincode文件到HTML文本转换器GitHub,该代码是驯服烂代码中的第四个实例。

第19章 驯服烂代码的步骤:IePpTr

IePpTr的定义

IePpTr表示Intent-ErrorProductionPassed-TotalRefactoring。字母组合TR前面的横线,表示这个TR所代表的全面重构,不仅需要实施到前面的字母P所代表的Production Code上,而且还需要实施到前面的字母I所代表的TODO、用户意图测试和意图代码上。

如果把TDD开发方式必做一种接口,那么IePpTr就是TDD的一种实现。

IePpTr的步骤

  1. 听其言:Intent(TODOs + User Intent Tests + Intention Code)

  2. 观其行:Test Run Error Fixing(Compiler Errors + Test Run Errors),Production Code,Production Code,Test Run Passed

  3. 守其道:Total Refactoring

章节总结

  1. 驯服烂代码的IePpTr方法是TDD开发方式的一种实现,可以分为3步:听其言(维护TODO列表、编写用户意图测试和意图代码)、观其行(以修复编译错误和测试运行错误为指引编写恰好够用的生产代码,直至测试运行通过)、守其道(全面重构TODO列表、测试代码和生产代码)。

  2. Total Refactoring 全面重构,是为了适应软件开发过程中软件外在行为会逐渐发生变化的情况,而对传统重构概念所做的扩展。这种扩展强调在遵从软件开发的“道”的前提下,对记录未完成任务的TODO列表、测试代码和生产代码进行修改,以改进代码的内部结构的过程。

第20章 习惯出自专注、长期和用心的结对操练

  1. 习惯是不能被消除的,而只能被代替。

  2. 如果把旧习惯的回路中的第1步中的“暗示”和第3步中的“奖赏”保留不动,然后用一个新的惯常行为替换掉第2步中旧的惯常行为,那么大脑就可以在第一步的结尾处选择我们的新习惯,从而养成这个新的习惯。比如,原先第1步的“暗示”是:打开计算机,启动IED准备编程,第2步“行为”是:完成生产代码的编写,之后再编写一个main()方法来测试一下,第3步“奖赏”是:看到运行输出结果符合预期。可以将第2步的“行为”替换为TDD的开发方式。这样保留第1步第3步,我们就可以最大限度的让程序员接受这个新习惯。

  3. 如果想让我们新的惯常行为达到世界级的专业水平,我们需要花至少10000小时来实践,即每天花3小时,连续进行10年的实践。

  4. 只有专注、长期和用心地去做这10000小时的实践,才有可能让我们新的惯常的行为达到专业高手的水平。

  5. 只要我们加入一个能让我们相信改变是可能的团体,即使这个团体只有两个人,也能让我们的新习惯永久地固化下来,并杜绝旧习惯的复发。

你可能感兴趣的:(编程,代码,TDD,测试驱动)