[转]读《修改代码的艺术》
正文之前
- 现实中的软件系统几乎总是会慢慢变为一个烂摊子。程序员脑子里原先那些漂亮的设计随着时间的推移会慢慢“发出腐化的臭味”
- 需求总是在改变。那些不能适应未来需求变更的设计是糟糕的设计。能够适应未来需求变更的设计是每一位合格的软件开发者的目标
- 实际上,迄今为止人们构建出的几乎所有软件系统都遭遇了缓慢的、不可抗拒的腐化
- 仅是努力防止腐化是不够的,你必须设法扭转它
- 遗留代码就是那些没有编写相应测试的代码
- 没有编写测试的代码是糟糕的代码。不管我们有多细心的去编写它们,不管它们有多漂亮、面向对象或者封装良好,只要没有编写测试,我们实际上就不知道修改后的代码是变得更好了还是更糟了。反之,有了测试,我们就能够迅速、可验证的修改代码的行为
- 在没有相应的测试的情况下就进行大规模的修改是要冒很大风险的。这就好像在没有防护网的情况下进行高空体操表演
- 只要代码没有编写相应的测试,其进行修改时的速度比不上那些有测试的团队
- 你可以在遗留代码基上“培养”出高质量的代码,不过倘若你在修改的某些步骤中发现某些代码变得比原来更丑陋了,千万别感到惊讶。因为这就像动手术一样,先开一个切口,进而在五脏六腑中动手术,先别管是否美观。
- 编程可以是一项回报丰厚并让人感觉是一种享受的工作
第一部分 修改机理
第1章 修改软件
- 保留既有行为不变是软件开发中最具挑战性的任务之一
第2章 带着反馈工作
- 精湛的软件改动就像精湛的外科手术一样,除了细心之外还要有深厚的技术。如果没有辅以正确的工具和技术,即便“小心下手”也起不到多大作用
- 单元测试是用于对付遗留代码的极其重要的组件之一。系统层面的回归测试的确很棒,然而相比之下,小巧而局部性的测试才是无价之宝,它们能够在进行改动的过程中不断给你反馈,使重构工作的安全性大大增强
- 当一个类直接依赖于某些难以在测试中使用的东西时,这个类就是难以修改和处理的
- 当在遗留代码中解依赖时,你尝尝不得不暂时将自己的审美感放在一旁。有些依赖能够干净利落的解除,而有些从设计的角度来看最终还是解决得不那么完满
- 在每次编完程之后,我们应当不仅能够指出哪些提供了某些新特性的代码,还要能够指出其测试。随着时间的推移,代码基受测试部分将会变得越来越大,就好像在海里不断生长的岛屿一样。在这些“岛屿”之上工作会容易得多。渐渐地,岛屿变成更大的陆地。最能你将能够在一块全面由测试覆盖的代码大陆板块上工作。
- 依赖性往往是进行测试的最为明显的障碍
第3章 感知和分离
- 如果我们想要将测试安置到位,有两个理由去进行解依赖:感知和分离
- 感知:当我们无法访问到代码计算出的值时,就需要通过解依赖来“感知”这些值
- 分离:当我们无法将哪怕一小块代码放入到测试用具中去运行时,就需要通过解依赖将这块代码“分离”出来
- 在编写测试时,我们得采取分而治之、逐个击破的方案。
- 遵循为独立单元编写测试的理念,我们最终写出来的就会使短小而易于理解的一个个测试。这会使得我们的代码变得更易理解
第4章 接缝模型
- 几乎每个尝试过为既有代码编写测试的人都会发现既有代码对测试是多么的不友好
- 要想最终得到易于测试的程序,边开发边编写测试更有效
- 接缝(seam),指程序中的一些特殊的点,在这些点上你无需做任何修改就可以达到改动程序行为的目的
- 在产品中过度使用预处理并不是个好主意,这会降低代码的清晰性。条件编译指令几乎等于是在强迫你同一份源代码中维护多个不同的程序
- 当遇到一个接缝时,即意味着我们可以改变其所在处的行为。我们不能仅仅为了测试就真的去修改其所在之处的代码,源代码在产品阶段和测试阶段应当是完全一样的
- 每个接缝都有一个激活点,在这些点上你可以决定使用哪种行为
- 连接期接缝的激活点从来都是位于程序代码之外的,并不是那么醒目,应该确保测试和产品环境之间的差别是显而易见的
- 当你为哪些特别脏乱的遗留代码安放测试时,往往最好的途径是尽量少去修改其代码。如果知道你的语言所支持的接缝类型,并知道怎样去使用它们,则通常可以更安全的将测试安置妥当
- 一般来说,如果你用的是面向对象语言,则对象接缝是最佳选择。预处理期接缝和连接期接缝某些时候是有用的,但他们没有对象接缝那么明显。依赖于预处理期接缝和连接期接缝的测试可能会难以维护
第5章 工具
- 重构:名词。对软件内部结构的一种调整,目的是在不改变软件的外在行为的前提下,提高其可理解性,降低其修改成本
第二部分 修改代码的技术
第6章 时间紧迫,但必须修改
- 虽说不管是解依赖还是为所要进行的修改编写测试都要花上一些时间,但大部分情况下最终还是节省了时间,同时也避免了一次又一次的沮丧感
- 测试可以“捕获”你在修改过程中不慎引入的错误,从而节省为此所花的时间;当试图寻找代码中的错误时测试也可以帮你节省时间。有测试在,我们常常就可以更容易的定位功能上的问题
- 通常情况下系统中的修改是相对集中的。今天修改了某处,很可能很快又要去修改附近的某地方了
- 代码就是你的家,你是得在其中生活的
- 往一个系统中添加特性且这个特性可以用全新的代码来编写时,将代码放在一个新的方法中,并在需要用到这个新功能的地方调用这一方法。你可能没法很容易将这些调用点置于测试之下,但至少可以为新编写的那部分代码进行测试
- 一个方法最初被建立起来时通常只为一个客户做单一的事情。此后往里面添加的任何代码某种程度上都是值得怀疑的
- 在对一个大的代码基进行改进时,最大的障碍就是现存代码。关键不在于那些难对付的代码要花多少工夫去对付,而是这样的代码给你带来的心理负担
第7章 漫长的修改
- 一个维护良好的系统和一个遗留系统之间有一个显著的区别:对于前者,你可能要花上一点时间来想想该如何修改,一旦想清楚了,改起来往往很容易,改完后的系统也感觉舒服多了。而在一个遗留系统中,可能得花上很长一段时间来搞清楚应该怎么做,同时修改起来往往也不容易。此外你可能还会觉得,除了因为修改而必须去理解的那一小块代码之外,并没有了解到多少其他东西。最糟的情况是,需要预先理解的代码好像不管花上多少时间都理解不完似的,最后你不得不闭着眼睛冲进代码,心里默默祈祷自己能够搞定所有将要遇到的问题
- 接口的改动频率通常情况下要远远低于接口背后的那些代码
- 让代码依赖于接口或抽象类,而不是依赖于具体类
- 当你可以快速编译并运行测试时,在开发过程中便能够获得更佳的反馈
- 当开始优化平均构建时间时,最终将会得到一块块非常易于工作的代码。使一小组类能够单独编译和测试这件工作做起来可能有点痛苦,但重点是,这是一劳永逸的。一旦完成这件工作,后面就能永远享受它带来的好处
第8章 添加特性
- 测试驱动开发的最有价值的一个方面是它使得我们可以在同一时间只关注于一件事情。要么是在编码,要么是在重构;永远也不会在同一时刻做两件事情。这一好处对于对付遗留代码的人们来说显得尤其有价值,因为它使我们能够独立的编写新代码。在编写完一些新代码之后,我们便可以通过重构来消除新旧代码之间的任何重复
- 重命名类能够改变人们看待代码的方式,并使他们注意到一些以前可能从未考虑过的可能性
- Liskov置换原则(LSP):子类对象应当能够用于替换代码中出现的它们的父类对象,不管后者被用在什么地方。这意味着一个给定类的客户代码应当能够在毫不知情的情况下使用该类的任何子类对象。
- 如何避免违反Liskov置换原则?
- 尽可能避免重写具体方法
- 倘若真的重写了某个具体方法,那么看看能否在重写方法中调用被重写的那个方法
- 当我们过于频繁的重写具体方式时,代码就容易变得混乱
- 在一个规范化的继承体系中,任何类都不会包含同一方法的多个实现。任何类都不会重写其父类中的具体方法。在一个规范化的继承体系中,无需担心子类会重写从它们的父类那儿继承来的行为。当然偶尔重写一两次具体方法其实也是无伤大雅的,只要不违反Liskov置换原则
第9章 无法将类放入测试用具中
- 不到万不得已千万别在产品代码中传递null。空对象模式用于避免在程序中传递null
- 当调用方无需关心某个操作是否成功时,空对象模式尤其有用
- C++中不要在构造函数中调用虚函数
- 如果你发现一个全局变量真的被到处使用,这意味着你的代码没有进行任何层次化设计
第10章 无法在测试用具中运行方法
- 如果需要测试一个私有方法,那么就应该将它设为公用的。如果不大方便将其设为公用的,则大多数情况下意味着我们的类做的事情太多了,应该进行适当调整。
- 好的设计应该是可测试的,不具可测试性的设计是糟糕的
- 将依赖于GUI的代码与不依赖于GUI的代码分离开
- 命令/查询分离:最先由Bertrand Meyer提出的设计准则。一个方法要么是一个命令,要么是一个查询;但不能两者都是。命令式方法指那些会改变对象状态但并不返回值的方法,而查询式方法则是指那些有返回值但不改变对象状态的方法
第11章 修改时应当测试哪些方法
- 当需要在特别错综复杂的遗留代码中做改动时,通常需要先花点时间考虑下应当在哪编写测试
- 代码影响代码的最难以察觉的方式便是通过全局或静态数据了
- 好的代码中是没有多少“陷阱”的
- 对于早已写成的遗留代码你也别指望能够从头来过了
第12章 在同一地进行多处修改,是否应该将相关的所有类都解依赖
第13章 修改时应该怎样写测试
- 几乎每一个依赖于手动测试的团队最终都远远落在了后面
- 试图使一个类变得可测试这一行为本身往往能够改善代码的质量
第14章 棘手的库依赖问题
- 尽量避免在你的代码中到处出现对库的直接调用。你可能会觉得永远也不会需要去修改这些调用,但最终可能只是自欺欺人
第15章 到处都是API调用
- 要么买,要么借,要么就自己开发一个。这是每个软件开发者都需要面对的选择
- 几乎每个系统当中都有一些核心逻辑是可以与API调用分离开来的
第16章 对代码的理解不足
第17章 应用毫无结构可言
- 整个团队就像绷紧的弹簧一样,光顾着埋头解决一个有一个的紧急情况,根本无暇顾及什么整体认识了
- 架构师不是少数人所专有的,而必须是大家的
- 正因为我们在考察系统的时候瞻前顾后束手束脚,所以往往会错过一些能够给我们额外启发的地方
第18章 测试代码碍手碍脚
- 如果你选择将测试产品代码分离开来,那么请确保你有充分的理由这么做,因为有些团队常常会为了“漂亮”而把它们分开,他们觉得让测试代码跟产品代码待在一起是没法接受的。结果后来在浏览项目的时候就会发现麻烦了。实际上把测试代码跟产品代码放在一起也没那么“丑陋”,一段时间之后你就习以为常了
第19章 对非面向对象的项目,如何安全的对它进行修改
- 宁可引入新的函数也不要把代码直接添加到旧代码中。因为至少可以给我们引入的新函数编写测试
第20章 处理大类
- 单一职责原则(SRP):每个类应该仅承担一个职责:它在系统中的意图应当是单一的,且修改它的原因应该只有一个
- 编写小型语言解释器的程序员们通常会把解析模块和表达式求值并在一起,一边解析一边求值。虽然这种做法比较方便,但随着语言的演化,其在可伸缩性上面的不足就会暴露出来了
- 遗留代码比起新加特性来说,它提供了多得多的运用设计技能的机会。当会被你的设计所影响的代码真实的存在眼皮底下时,谈论起设计权衡来回更容易一些,也更容易知道某个结构在特定上下文中是否合适,因为上下文就真实的存在于我们面前
- 观察隐藏方法:注意那些私有或受保护的方法。大量私有或受保护的方法往往意味着一个类内部有另一个类急迫的想要独立出来
- 如果你迫切需要测试一个私有方法,那么该方法就不应该是私有的;如果将它改为公有会带来麻烦,那么可能是因为它本来就应属于另一个独立的职责。它应该在另一个类上
- 接口隔离原则(ISP):如果一个类体积较大,那么很可能它的客户并不会使用其所用的方法。通常我们会看到特定用户使用特定的一组方法。如果我们给特定用户使用的那组方法创建一个接口,并让这个大类实现该接口,那么用户便可以使用“属于它的”那个接口来访问类了。这种方法有利于信息隐藏,此外也减少了系统中存在的依赖
- 如果发现你自己正在为某件事情提供另一条解决方案,那么可能意味着这里面存在一个应该被提取并允许替代的职责
- 几乎还无例外的,当一个团队进行一番大规模重构时,系统稳定性就会有一段时间的下跌,即便他们干得再仔细,一边编写测试一边重构,还是如此。如果仍处于发布周期的早期,愿意承担这样的风险,且有时间,那么一次大规模重构也并非不可。只要记得别让那些bug搞得你不再想去做其他重构就是了
- 在没有测试的情况下进行类提取可能会遇到一些不测。其中最难觉察的就是可能会引入与继承有关的bug。基类中同名方法和成员变量会在子类移除后显露出来。
第21章 需要修改大量相同的代码
- 类名和方法名缩写是问题的来源之一
- 开放/封闭原则:代码对于扩展应该是开放的而对于修改则应该是封闭的。对于一个好的设计,我们无需对代码做太多的修改就可以添加新的特性
第22章 要修改一个巨型方法,却没法为它编写测试
- 自动重构工具的支持可以让你无需做任何特殊准备就开始对大方法进行分解。好的重构工具能够帮你检查试图进行的重构是否安全。但如果没有重构工具,就得靠自己来确保重构的正确性,这时测试便成了最强的工具。
第23章 降低修改的风险
- 编程是关于同一时间只做一件事的艺术
- 编程的时候一不小心就会掉入“贪心不足蛇吞象”的局面,结果是不仅受到打击,而且落到只能通过尝试来让代码工作的境地,而不是胸有成竹
- 结对编程对于提高质量以及在团队中传递知识都是很有好处的
- 我们在编辑代码时很容易犯错误,并且自己还根本不知道已经破坏了代码。而多一双眼睛看着当然是有好处的。对付遗留代码就好比是动手术,而医生是从来不会一个人做手术的
第24章 当你感到绝望时
- 如果你的团队士气低迷,而且是由于代码质量太低而低迷的,那么有个办法可以一试——从项目中找出一组最糟糕的类,将它们置于测试之下。一旦你们作为一个团队共同克服了一个最困难的问题,那么就会感觉一切都不过如此了
第三部分 解依赖技术
第25章 解依赖技术
- 一旦测试到位了,你也就可以更放心的去做一些更为侵入性的改动了
- 遗留代码基中的一个普遍问题就是抽象层次不够;系统中最重要的代码往往跟底层API调用耦合在一起
- 安全第一,一旦测试到位,你便可以更有信心的进行侵入性的改动了
- 一个类的静态区段可以看做是“临时场地”,用于存放不是十分隶属于该类的东西。如果你看到某个方法没有使用任何实例数据,那么把它设成静态的是个好主意,这样可以让它变得醒目,直到你弄清它应该属于哪个类
- 命名是设计的关键部分。好的名字有助于人们理解系统,并令系统更以对付。反之,糟糕的名字会影响理解,并给你身后的程序员带来无尽烦恼
- 单件模式能够方式人们在产品代码中创建不止一个目标类的实例,但它同样阻止了人们在测试用具中创建第二个实例。这是一把双刃剑
- 更好的方向:减少对单件的全局引用,最终使单件类可以成为一个普通类
- 手术也从来都不是漂亮的,尤其是开始的时候
- C++中私有虚函数仍然是可以在子类中重写的


浙公网安备 33010602011771号