`

<修改代码的艺术>读书笔记

    博客分类:
  • Book
阅读更多

这本书看的时间非常长, 断断续续有3个星期了吧, 不错的书, 至少对我来说是这样, 因为我现在就碰到了书中列出的种种问题:对已有的没有完善的单元测试的核心系统进行重构.为了保证少出乱子, 不出乱子, 我必须小心的对超大类, 巨型方法采用各种重构手段进行修改, 没有单元测试作保证的系统进行重构是非常危险的事儿, 那怎么办呢, 这本书让我知道了不少好的方法.

与<重构到模式>一样, 这也是一本围绕重构来阐述如何修改代码, 只不过它面对的场景是遗留系统.对遗留系统进行重构首先要做的就是要让我们的修改能够被单元测试用具夹住, 就像对一栋居民楼进行改造一样, 必须先安装脚手架, 拉上防护网, 做好安全措施, 然后再开工. 从而减少因为修改带来的风险.

如何在修改代码前搞定对应的单元测试呢?这是一个非常有技巧的活儿, 因为遗留系统可能因为设计不佳而无法或者很难非常容易的为其修改部分添加单元测试. 之所以造成这样的局面一个重要的方面是无法对要测试的部分进行解除依赖. 因此解除相关依赖也就成了修改代码首先面对的一个难题, 本书在解除依赖方面总结了非常多的方法(或模式),  这些方法总有一款会适合你. 本人觉得这应该算是这本书的所要告诉我们的主要内容吧.

本书的例子大部分都是以java为主, 少量的穿插了c++的实例, 而且有些手法跟语言的关系比较大, 可能适用c++的做法, 对java并不适用.

另外本书给我的另一个启发就是, 我们在写代码的时候, 应该尽量做好单元测试, 单元测试可能在最初实现的时候, 作用相对来说比较小, 但是在后期维护修改重构以及添加新功能的时候, 其威力将逐渐显现出来. 此外一些系统之所以不好修改, 后期维护困难, 与我们编码时候没有遵循一定的规则有关, 比如依赖管理, 代码复杂度控制, 这些规则其实都非常简单, 人人都会知道, 但是在具体场景能不能想到, 并加以灵活运用却是另外一回事儿, 我觉得这个必须经过大量的编码实践才能找到这种感觉, 就像学习英语, 只有多读, 多听, 多说, 才能具有一定的语感, 从而最终掌握.

一些摘记

单元测试是用于对付遗留代码的极其重要的组件之一 系统层面的回归测试的确很棒, 然而相比之下, 小巧而局部性的测试才是无价之宝, 他们能够在进行改动的过程中不断给你反馈, 使得重构工作的安全性大大增强.

好的单元测试所具有的品质:
  • 运行快
  • 能帮助我们定位问题所在.


如果我们的测试相当大, 其执行时间可能长的令人无法忍受, 可想而知我们更可能做的事情便是尽量避免去运行它, 于是就无法达到错误定位的目的.
一个需要耗时十分之一秒才能执行完的测试就已经算是一个慢的单元测试了.

单元测试运行得快, 运行的不快的不是单元测试
有些测试容易跟单元测试混淆起来, 譬如下面这些测试就不是单元测试:
  • 跟数据库有交互
  • 进行网络通信
  • 调用了文件系统
  • 需要你对环境做特定的准备(如编辑配置文件)才能运行.

当然, 这并不是说这些测试就是坏的, 编写他们常常也是有价值的, 而且你通常也会在单元测试用具类来编写他们. 然而, 将他们与真正的单元测试区分开来还是很有必要的, 因额外izheyang你就能够知道哪些测试是你可以快速运行的.

修改代码的一般步骤:
  • 确定修改点
  • 找出测试点
  • 解依赖
  • 编写测试
  • 修改, 重构.


感知和分离
通常, 如果我们想要将测试安置到位, 有两个理由去进行解依赖:感知和分离
感知:当我们无法访问到代码计算出的值时, 就需要通过解依赖来感知这些值
分离:当我们无法将哪怕很小的一块代码放入测试用具中去运行时, 就需要通过解依赖将这块代码分离出来.

接缝模型
就是通过同样的类, 但是包路径不一样, 通过修改classpath来达到解除依赖的目的,  这是java的做法.还有一种就是对象接缝, 就是将要解依赖的地方提取到一个单独的protected的方法中, 然后通过继承实现一个测试类来替换掉相关的依赖.

有时候在对遗留代码进行修改的时候, 要采用一些迂回的做法, 这样看起来的确很烦, 但是没办法, 我们应该尽量减少对已有代码进行修改, 要淡定淡定再淡定, 尽量避免修改的冲动^_^

任何时候, 只要你发现新添加的功能可以写成一块独立的代码, 或者暂时还没发用测试来覆盖待修改方法时, 可以采用新增方法的方式, 这样比直接往原方法中添加代码好多了.

另一个做法就是新生类, 即创建一个新的类来容纳要进行的改动, 并在院内中使用这个新类.

测试驱动开发的最优价值的一个方面是它使得我们可以在同一时间只关注于一件事情, 要么是在编码, 要么是在重构, 永远也不会在同一时间做两件事.这一好处 对付遗留代码的人们来说显得尤为有价值, 因为它使我们能够独立地编写新代码, 在编写完一些新代码之后, 我们便可以通过重构来消除新旧代码之间的任何重复.

有许多重构手法都是相当强大的, 但重命名是其中最为强大的一项, 它能够改变人们看待代码的方式, 并使他们注意到一些以前可能从未考虑过的可能性.

Liskov置换原则(LSP)规定:子类对象应该能够用于替换代码中出现的他们的父类对象, 不管后者被用在什么地方, 如果不能的话, 代码中就有可能悄无声息的出现一些错误, 正方形从矩形继承就是一个经典的违反LSP原则的案例.

LSP意味着一个给定类的客户代码应该能够在毫不知情的情况下使用该类的任何子类对象, 不存在任何机械性的方法来避免违反该原则. 一个类是否符合LSP取决于它的客户代码, 以及这些客户代码对代码行为或结果的期望. 不过, 这里存在着一些一般的规则:
尽可能避免重写具体方法
尚若真的重写了某个具体方法, 那么看看能否在重写方法中调用被重写的那个方法.

好的设计应该是可测试的, 不具有可测试性的设计是糟糕的.

查询/命令分离原则:一个方法要么是一个命令, 要么是一个查询;但不能两者都是.命令方式方法指那些会改变对象状态但并不返回值的方法, 而查询式方法则是指那些有返回值但不改变对象状态的方法. 该原则的重要之处在于, 它向用户传达的信息, 如果一个方法是查询式的, 那么无需查看其方法体就知道可以连续多次使用它而不用担心它带来什么副作用.

信息隐藏是件好事, 只不过, 若是被隐藏的是我们需要知道的信息就不妙了.

影响在代码中的传递主要有三种途径:
调用方使用被调用函数的返回值
修改传参传进来的对象, 且后者解析来会被使用到
修改后面会用到的静态或者全局数据.

寻找修改造成的影响时使用的方法:
确定一个将要修改的方法
如果该方法有返回值, 查看它的调用方
看看该方法是否修改了什么值, 是则查看其他使用了这些值的方法, 以及使用这些方法的方法
别忘了查看父类和子类, 他们也可能使用这些实例变量和方法
找出到目前为止被你所找出的任何方法修改的全局变量和静态数据.

我们对影响的扩大范围的限制越是严厉, 编起程序来就越是容易.

许多解依赖技术都会对对象的封装性造成破坏. 封装的重要性毋庸置疑, 然而更重要的是它的重要性背后的原因, 封装有助于我们对代码进行推测.  跟封装性不佳的代码相比, 理解封装良好的代码所需要跟踪的路径更少. 例如, 假设我们要给一个构造函数添加一个参数来达到解依赖的目的(参数化构造函数), 这在推测影响的时候可能需要多考虑一条路径了, 没错, 打破封装会令代码中的影响推测变得更困难, 然而若是最终我们能够给出具有很好说明和阐释作用的测试, 情况就恰恰相反了.

我们在给新代码编写单元测试的时候, 要尽可能单独而孤立地去测试他们, 一旦意识到手头的测试已经过于笨猪了, 就应该去分解被测试类, 分解出容易测试的独立小类来. 另外我们还不时 去伪造被室内需要的某些对象, 因为单元测试的任务并非检查一簇类是否能够合作良好, 而是检查单个的对象行为是否正确. 通过伪造合作类对象我们就可以更容易的达到这一目的.

在给既有代码编写测试时, 情况刚刚相反, 比较好的做法是把某一块代码切割下来, 利用测试来固定它, 当这些测试都安置到位之后, 我们就可以更容易地为这块区域内的每一个类编写更狭窄的单元测试了.

重构的时候我们通常需要关心两件事:一是目标行为在重构之后是否依然存在, 二是它是否正确连接到系统中.

如何编写特征测试:
为准备修改的代码区域编写测试, 尽量编写用例, 知道觉得你已经理解了那块代码的行为.
之后再开始考虑你所要进行的修改, 并针对修改编写测试.
如果想要提取或者转移某些功能, 那就编写测试来验证这些行为的存在性和一致性, 一种情况一种情况的编写.

草稿式重构
你走进代码, 一般捣鼓之后, 代码变得更清晰了, 唯一的问题是, 如果没有测试, 这个过程会令人相当抓狂.从代码控制系统中输出代码, 然后别管测试编写的事情, 只管去提取方法, 移动变量, 以你喜欢的方式进行重构, 只是别提交到服务器, 这种做法叫"草稿式重构", 它可以帮助我们理解代码的本质, 以及认识代码是如何工作的.

讲系统故事
采用一问一答式, 比如一个问, 该系统的架构是怎样的? 然后另一个人回答, 应该尽量只使用两三个概念就把系统的架构解释清楚. 阐述包括两个方面:系统的组成和他们之间是如何交互的. 通过简单清楚的表述系统最为本质, 核心的东西. 然后接着再选择第二重要的方面来讲述.
比如对JUnit的系统结构的表述:
Junit包含两个主要的类:一个叫Test, 一个叫TestResult, 用户创建测试并运行他们, 同时传递一个TestResult给他们, 如果一个测试失败了, 他就会把情况告诉TestResult. 于是人们就可以通过问TestResult对象来了解发生的所有失败情况.

处理大类
单一职责原则(SRP):每个类应该仅承担一个职责:它在系统中的意图应当是单一的, 且修改它的原因应该只有一个.

职责识别
有时候可以通过方法名来对类的职责进行分组.将所有方法都列出来, 然后就思考他们的意图是什么, 我问自己两个关键性的问题是, 这个方法为什么在这儿, 它为这个类做了什么?接着把这些方法分组, 将具有相近目的的方法放在一起.

识别职责是一个关键的设计技能, 而且需要锻炼才能掌握.

注意那些私有或者受保护的方法, 大量私有或受保护的方法往往意味着一个类内部有另一个类急迫想要独立出来

庞大的类可能隐藏很多东西, 如果你迫切需要测试一个私有方法, 那么该方法就不应该是私有的, 如果将其改成公有的会带来麻烦, 那么可能是因为它 就应属于另一个独立的职责, 应该在另一个类上.

寻找可以更改的决定
寻找代码中的决定---这里说的决定并非你正在做的决定, 而是指已经做出的决定, 比如代码中有什么地方(与数据库交互, 与另一组对象交互, 等等)采用了硬编码么? 你可以设想他们发生变化后的情况么?

寻找内部关系
寻找成员变量和方法之间的关系, "这个变量只被这些方法使用吗?"

寻找主要职责
尝试仅用一句话来描述该类的职责.
单一职责原则的违反有两种形式, 一种是接口层面的违反, 一种是实现层面的违反, 然而我们最关心的还是实现层面的违反, 简单的说, 我们关心的就是该类是否真的做了这些事情.还是仅仅将其委托给其他的类来完成. 如果属于后者, 那么该类并不能算是一个巨大的单片类, 而只能算是一帮小类的facade.

接口隔离原则(ISP)
如果一个类体积超大, 那么很可能它的客户并不会使用其所有方法. 通常我们会看到特定用户使用特定的一组方法. 如果我们给特定用户使用的那组方法创建一个接口, 并让这个大类实现该接口, 那么用户便可以使用属于它的那个接口来访问我们的类了. 这种做法有利于信息隐藏, 此外也减少了系统中存在的依赖.

注意你目前手头正在做的事情, 如果发现你自己正在为某件事提供另一条解决方案那么可能便意味着这里面存在一个应该被提取并允许替代的职责.

要真正很好的认清代码内部的职责, 还是只有多读其他的开源项目的代码, 读的时候注意别人是怎么命名各个系统中的类的, 此外还是类名和方法之间的对应关系.

对一个庞大的类进行提取通常是一个好的开始, 实践当中我们发现, 团队在做这个工作的时候面临的最大的危险就是野心过大.

修改巨型方法
长方法就像代码基中的沼泽, 不管什么时候, 只要你试图去改变它, 就得退一步先努力把情况弄清楚, 然后再去修改.

在没有测试的情况进行自动重构时, 一定要只用工具进行(不要掺杂手工修改), 而在一系列的自动重构完成之后, 往往就可以将测试安置到位, 并用这些测试来验证你所进行的任何手动修改了.

引入感知变量
我们可能不希望在重构的时候向代码中引入新的特性, 但并不表示不能向代码中添加任何内容, 比如我们可以往类中添加一个变量用来感知待重构方法内的条件, 而完成了重构之后则可以将变量删除, 于是我们的代码又回到了干净的状态, 这一手法叫做引入感知变量

感知变量是分解巨型方法的利器.

对一个巨型方法的策略就是一开始迈小步, 寻找那些我们可以不用测试也能放心提取出来的小块代码, 然后添加测试来覆盖它, 这里的小一般指两三行, 最多五行的代码, 一块你能很容易给其想出名字的代码.

一般来说, 最好提取那些耦合数小(传入, 传出参数数目之和)的方法, 因为这样犯错误的几率会小一些.

在从一个巨型方法中提取代码时, 你可能会注意到其中有些代码块其实是应该属于其他类的, 对此一个很强的暗示就是理想给新方法取的名字

同一时间只做一件事情.

解依赖技术
当某个测试类很难在测试用具中被实例化, 那么看看能否将其改成静态的.

代码的静态部分从某种程度来说, 它其实并不属于该类.

提取并重用(从要测试类继承, 然后重载要屏蔽的依赖)是个非常有用的重构方法, 如果你的目的是解开对全局变量和静态方法的依赖, 它是个理想的选择.

对构造函数中的初始化工作可能会给测试带来麻烦, 此时可以采用提取并重写工厂方法

对静态方法进行解依赖的一个比较好的做法:引入实例委托.
找出在测试中带来问题的那个静态方法
在它所属类上新建一个实例方法, 让该实例方法委托那个静态方法.

解除对全局变量(单例对象)的依赖, 方法之一就是对每个相应的全局变量引入一个获取方法, 有了这个获取方法之后, 就可以通过子类化并重写方法来令他们返回测试用的对象了.

子类化并重写方法:是面向对象程序中解依赖的核心技术, 很多解依赖手法都是该手法的一种变种. 该手法的核心理念就是你可以在测试环境下利用继承来讲并不关心的行为架空或访问到你所关心的行为.
分享到:
评论

相关推荐

    JavaScript_DOM_编程艺术读书笔记

    ### JavaScript DOM 编程艺术读书笔记关键知识点解析 #### 一、JavaScript简史与相关技术简介 - **XHTML(可扩展的超文本标记语言)**:这是一种更加严格、更加强大的HTML版本,旨在提高网页的可读性和可扩展性。 ...

    《JavaScript DOM 编程艺术》读书笔记之DOM基础

    在《JavaScript DOM 编程艺术》读书笔记之DOM基础中,我们将深入了解文档对象模型(Document Object Model,简称DOM)的基本概念、结构及JavaScript中的DOM操作方法。这些知识点对于前端开发人员来说至关重要,因为...

    《JavaScript DOM 编程艺术》读书笔记之JavaScript 图片库

    例如,如果你想修改`&lt;p&gt;`元素内的文本,可以这样操作: ```javascript var description = document.getElementById('description'); // 获取第一个子节点(假设它是文本节点) var firstChild = description....

    Android代码-一个集Gank.Io,Rxjava示例,操作符,MD控件使用,各种好玩Ap示例的学习App。

    安卓艺术开发探索读书笔记,EffectiveJava读书笔记. 收集各大神博客以及安卓笔记,安卓面试笔记等方便手机端查看. 更新说明 v2.2.2 1.增加了一些最近在学习比较好的安卓大神的博客. 2.增加了来自GeniusVJR整理的安卓...

    《JavaScript DOM 编程艺术》读书笔记之JavaScript 简史

    以上内容总结了《JavaScript DOM 编程艺术》第一章的读书笔记,概括了JavaScript简史,从而让我们了解到这一语言的发展脉络,以及它如何从最初的技术竞争中走向标准化,并最终成为现代Web开发不可或缺的一部分。

    NFT艺术品随机生成器源码

    这为开发者提供了一个模板,他们可以根据自己的需求扩展或修改这个项目,比如添加更多元素、调整随机策略或者引入更复杂的艺术风格。 压缩包中的“说明.txt”文件可能包含了项目的说明、使用指南或开发者笔记,帮助...

    clean-code-booknote:记录clean-code的读书笔记

    这个压缩包文件"clean-code-booknote"显然是一个读者对这本书的读书笔记,可能包含了书中关键概念、原则和实践的总结。虽然没有具体的标签提供额外信息,我们可以根据书的主题来深入讨论一些软件开发中的核心知识点...

    jQuery电脑鼠标翻书手机触屏滑动翻书代码.zip

    《jQuery电脑鼠标翻书手机触屏...无论是用于在线阅读平台、产品展示还是艺术展览,这种效果都能为网站增添独特的魅力。通过学习和理解这套代码,开发者可以进一步提升自己的前端技能,特别是在交互设计和动画实现方面。

    day2_大宋_

    【标题】"day2_大宋_" 暗示了我们可能在讨论一个与宋代文化或者历史相关的项目,或者是以“大宋”为背景的某种数字化产品...为了更深入地掌握这个项目,我们需要解压这些文件,阅读资料和笔记,理解并分析其中的代码。

    MyArtProgress:我的艺术的备份

    这个系统可能包含了代码、图片、文档等多种形式的艺术作品,使得用户能够方便地管理和追踪自己的艺术发展过程。在深入探讨这个项目之前,我们先来了解一些基本概念和技术背景。 首先,备份是数据安全的重要环节,它...

    火爆全网的AI绘画壁纸视频课程视频课程下载整理.zip

    6. **源代码实践**:可能包含的源代码文件让你有机会直接运行和修改现有的AI模型,以便于理解和改进。 7. **STM32和C语言**:虽然主要标签提到这些,但在AI绘画壁纸的课程中,STM32微控制器和C语言的应用可能较少,...

    c语言做的绘图板系统.zip

    解压后,用户可以深入了解系统的内部工作原理,开发者如何利用C语言实现GUI功能,以及如何扩展或修改现有代码。 “c语言做的绘图板系统”文件可能就是实际的可执行程序,用户可以直接运行来体验绘图功能。如果配合...

    sketches:生成草图

    生成艺术草图。 新的先来 假面 基于此,重新使用调色板草图(如下)进行颜色量化。 几个月前,我写了这篇文章,打算重写并加以改进,但从未实现。 由于时间已经过去了很多,所以我决定发布它,也许有人觉得它有用。...

    超级AI大脑开源聊天机器人源码+自动绘画等AI功能.zip

    开源的意义在于它允许开发者查看、修改并分发代码,促进了技术创新和社区协作。这款超级AI大脑的聊天机器人采用了先进的自然语言处理(NLP)技术,可能包括深度学习模型如Transformer或LSTM,用于理解和生成人类语言...

    测试基础知识

    - 《软件测试的艺术》读书笔记可能包含了作者对测试的独特见解和实践经验,加深对测试理论的理解。 - "软件测试概述.ppt"可能是一个简洁明了的测试介绍,适合作为快速了解测试的参考资料。 通过深入学习这些文档...

    ditaa-开源

    这个工具特别适用于那些需要快速将简单的文本描述转换成图形表示的场景,比如在文档、笔记或者代码库中。它的存在极大地提高了代码和文本中图形的可读性和吸引力。 Ditaa的主要功能是解析ASCII字符,这些字符通常是...

    纸张效果博客模板

    【标签】"源码"意味着这个博客模板提供了源代码,允许用户深入定制和修改。这为有编程基础的用户提供了极大的灵活性,可以根据自己的需求调整布局、颜色、字体等元素,甚至添加新的功能。"工具"标签可能暗示了模板中...

    超级画板程序使用说明.zip

    阅读这份文档,我们可以了解到如何在Python环境中运行可执行程序,以及如何通过源代码进行定制和修改。 Python作为一门广泛使用的编程语言,其源码的学习对于理解程序逻辑、调试错误和提升编程技能至关重要。通过...

Global site tag (gtag.js) - Google Analytics