驯服烂代码

 

驯服烂代码

何为烂代码

遗留代码
  • 难以理解、难以修改的代码。
  • 没有编程测试的代码就是糟糕的代码。
  • 没写测试。
大泥球
  • 结构混乱、肆意蔓延、轻浮草率、贴满补丁、私搭乱建、一团乱麻的丛林。
  • 无序增长、反复修补、权宜修复的迹象。
  • 互不相干的部分之间杂乱地被共享。
  • 几乎所有的重要信息变成全局的或重复的。
  • 系统的总体架构可能从未被被定义过。
烂代码一般表现在下面几个方面
  • 命令不清。
  • 多层嵌套。
  • 滥用模式。
  • 代码冗余。
  • 难以扩展。
  • 不如重写。
烂代码应该具有下面3点特性
  • 烂代码都是能够运行的。不能运行的代码不能称为代码,只能称为一堆字符串。
  • 烂代码都需要修改其中的bug或者在其中增加新功能的。
  • 烂代码对于其编写者、测试者和维护者来说都是反馈迟缓的。

简而言之,烂代码就是反馈迟缓的代码。这里的反馈包括了了解代码的行为、验证代码、和在这些代码之上修改bug或新增功能等方面。

分而治之-釜底抽薪

对生成代码所做的重构无非是用新代码替换旧代码。替换的方法可以分为两种
  • 第一种,不保留旧代码,而直接用新代码替换旧代码,并最终保证原有的测试在新代码上也能运行通过,这种方法好比“釜底抽薪”,从根本上解决问题。
  • 第二种是在保留旧代码的基础上,在旧代码后面编写新代码,带新代码编写完,将代码行为切换到新代码后仍能保证测试运行通过,此时再删除旧代码,这种办法好比“抛砖引玉”,照着旧代码这块“砖”编写新代码这块“玉”。

“釜底抽薪”的方法适用于简单的重构。“抛砖引玉”的方法适用于复杂一些的重构。

分而治之-抛砖引玉

对于看起来复杂的系统,可以使用“抛砖引玉”的方法来进行
  • 即可以在目前的位置计算逻辑的后面编写意图代码,等新的意图代码编写完毕,将代码行为切换到它之上,并运行测试通过后,就可以删除原来的位置计算逻辑。
  • 使用该重构方法的好处是,能够最大限度地让测试频繁得到运行,让复杂的重构得到测试最大限度的保护。

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

分而测之-编写Stub及提取接口

  • 使用提取接口的方法,让SUT针对对象的接口,而不是针对具体的DOC来编程。先从DOC中提取接口,然后编写Stub类实现接口,并通过SUT的带有上述接口类型的构造器,将一个Stub对象注入到SUT对象中,从而实现让SUT把抑郁控制的Stub类当成上述接口来看待,对SUT进行测试。
  • 代码虽然易读,但由于没有测试保护,使其对于代码维护者来说反馈慢,所以这样的代码还是属于烂代码。
  • 当SUT所依赖的DOC很难再测试中进行控制时,可以根据DOC提取一个接口,然后让SUT不针对DOC,而是针对这个接口编程,并根据这个接口编写易于控制的Test Double,来替代那个难以控制的DOC,从而令SUT把Test Double当成那个接口来看待,进而对SUT进行测试,来解决DOC在测试时难以控制的问题。
  • 使用像ISensor这样命名接口的方式并不理想,比较好的方法是用translate.google.cn来寻找一个类名的英文近义词为该类接口命名。
如何解决被测系统(System Under Test,SUT)所依赖的组件(Depended-On Component,DOC)在测试中难以控制的问题
Stub和Mock
  • 两者都属于Test Double。
  • Stub的作用是让测试能够控制SUT的间接输入,以便于测试能够强制SUT进入正常情况下很难进行的运行路径中。
  • Mock的作用是让测试能够验证SUT的间接输出。

分而测之-编写Mock及子类化并复写方法

  • 一个面向对象编程语言所定义的具体的类,虽然在形式上不是一个接口,但如果该类有一个符合里氏替换原则的子类,那么在子类眼中,父类就可以被当成接口来使用。
  • Mock除了完成Stub所做的为SUT在测试中的运行提供间接输入外,还要额外做验证SUT在测试中的间接输出的事情,所以Mock累中一般都有verify()这样的验证方法。
  • 使用先编写意图代码,然后通过频繁运行测试,编写最少量的代码(如能让变异通过的尚未实现得空方法,和能让测试运行通过的上述空方法的最少量的实现代码)以修复编译和测试运行错误,来驱动出生产代码的开发方式,既然能让我们在编写意图代码时进行适当的设计,并能编写最少量的能让测试运行通过的生产代码,以减少浪费,又能让我们逐步熟悉这些信息当成指路的向导,避免盲目地编写生产代码。
  • 与手工编写的Mock相比,用Mockito框架编写的Mock又下列好处,不必编写额外的Mock类,减少工作量;凭借Mockito框架的诸如mock()、when()、verify()这些方法的精心设计,能够让Mock代码可读性更高。
  • 与用Mockito框架编写Mock相比,手工编写Mock也有下列好处,能够完全控制Mock的编写过程;熟悉手工编写Mock的过程后,有利于理解Mokc框架的使用方法。

真正的单元测试

单元测试运行的快。运行得不快的不是单元测试。一个需要耗时0.1秒才能执行完的单元测试已算是一个慢的单元测试了。

有些测试容易跟单元测试混淆起来。比如
  • 跟数据库有交互。
  • 进行了网络间的通信。
  • 调用了文件系统。
  • 需要对环境做特定的准备(如编辑配置文件)才能运行起来。

单元测试要测的仅仅是SUT里面的软件行为,并不是测试SUT于诸如数据库、网络和文件系统这些DOC之间是否能够正常交互。可以先找到DOC的接口,然后让SUT针对这个接口编程,并且编写一个运行起来经济快捷的Test Double来实现这个接口,并注入SUT中,让SUT把这个Test Double当成那个接口来使用。这样能够依赖于DOC的集成测试,转换为依赖于实现接口的Test Double的单元测试了。

  • 当不大了解要测试的SUT的实际行为时,使用了特征测试的方法来进行测试。即可以故意在断言中填写一个不正确的期望结果,然后让测试运行到这里。一般测试到这里会运行出错,并在出错的信息中给出实际值。此时可以把出错信息中的实际值填写到测试断言中的期望值那里,来让测试运行通过。
  • 运行速度慢的不是单元测试。于数据库、网络、文件系统和配置文件有交互的测试不是单元测试。
  • 单元测试要测试仅仅是SUT里面的软件行为,而不是测试SUT与诸如数据库、网络和文件系统这些DOC之间是否能正常交互。
  • 要把SUT与诸如数据库、网络、文件系统这些的DOC之间进行交互的集成测试,转变成针对SUT的单元测试,可以先找到DOC的接口,让SUT针对这个接口编程,并且编写一个运行起来经济快捷的Test Double来实现这个接口,并且注入SUT中,让SUT把这个Test Double当成那个接口来使用。这样就能把依赖于DOC的集成测试,转变成依赖于实现接口的Test Double的单元测试。

驯服烂代码的步骤:LePpTr

要了解一段烂代码,也不仅要听其言,即要看代码本身、文档及注释来理解代码的意图,更要观其行,即用运行测试代码的方式来验证代码是否言行一致。

  • 如果程序员把内心的“镜子”擦亮会怎样?就会“照”出像设计模式、面向对象的SOLID设计原则、重构、测试驱动开发、敏捷软件开发、精益这些软件开发的“道”。
  • 如果用昏暗的“镜子”照出“反模式”,当成软件开发的“道”。在编写代码中最典型的反模式就是图方便的复制和粘贴“CV大法”。
驯服烂代码的步骤如下
  1. 听其言,维护TODO列表、编写用户意图测试和意图代码。TODO列表就是意图列表。程序员可以手机并审查所有当前和今后要做的诸如用户意图测试、bug修复和diamante“腐臭”治理的任务、形成一个用意图来表达的TODO列表。然后根据当时的情况,或者选择一个条件已经具备且有信心完成的用户意图TODO作为下一步要开发的任务,把它标记为working-on,根据用户意图做出合理的分析和设计,根据设计意图编写用户意图测试或意图代码。可以把每条TODO都以代码注释的方式写到代码相关的位置,并且用IDE的TODO管理界面加以管理。对于完成的TODO,就可以从列表中删除。
  2. 观其行,以修复编译错误和测试运行错误为指引编写恰好够用的生产代码,直至测试运行通过。首先以上一步所编写的意图代码的红色编译错误为指引,当信心弱时可以编写尽量少的生产代码(即可以写空类或空方法),当信心强时可以写自认为合理的生产代码,来让测试代码和生产代码编译通过。然后运行测试,当发现测试运行中的诸如空指针这样的测试运行错误后,以这些错误为指引,同样当信心弱时编写少量生产代码,当信心强时编写自认为合理的代码,让测试运行。
  3. 守其道,全面重构TODO列表、测试代码和生产代码。首先“嗅一嗅”上一步令测试运行通过的生产代码中,是否有“腐臭”味道。如果有,进行下小步重构生产代码,消除“腐臭”。然后根据诸如面向对象的SOLID原则、已知的或更新后的产品特性和设计模式这些软件的“道”的层面的概念,来审查所有的测试代码和生产代码,来找出不合理的测试、代码“腐臭”、被遗漏的User Story或刚刚从测试工程师手上接到的bug报告等,并以TODO的形式记录下来,补充到“听其言”步骤中的TODO列表中,来进入下一个迭代。
全面重构的定义
  • 在遵从诸如面向对象设计的SOLID原则、当下更新后的产品特性和消除代码“腐臭”等这些软件开发的“道”的层面的概念前提下,对记录为完成任何的TODO列表、测试代码和生产代码进行修改,以改变代码内部结构的过程。
驯服烂代码的IePpTr方法是TDD开发方法的一种实现
  • 听其言。[维护TODO列表、编写用户意图测试和意图代码]
  • 观其行。[以修复编译错误和测试运行错误为指引编写恰好够用的生产代码,直至测试运行通过]
  • 守其道。[全面重构TODO列表、测试代码和生产代码]

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章