第11章 重构API
11.2 函数参数化(Parameterize Function)
第1章 重构,第一个示例
1.1 起点
1.2 对此起始程序的评价
若程序杂乱无章,先为它整理出结构来,再做需要的修改,通常更简单
再强调一次,是需求的变化使重构变得必要
1.3 重构的第一步
重构前,确认有一套可靠的测试集,这些测试必须有自我检验能力
编写测试需要花费时间,但却可节省可观的调试时间
1.4 分解 statement 函数
重构过程的精髓:小步修改,每次修改后就运行测试
修改->测试->提交到本地版本控制系统:每次微小的重构后都会提交代码,以便在搞砸后,轻松回滚到上一<br>可工作状态,把代码推送(push)到远程仓库前,我会把零碎的修改压缩成一个更有意义的提交(commit)
这是我个人的编码风格:永远将函数返回值命令名result,这样我一眼就能知道它的作用
这是我另一个编码风格:使用一门动态类型语言,参数取名时都默认带上其类型名,这习惯从Kent Beck学来
移除play变量
移除局部变量的好处是做提炼是会简单得多,因为需要操心的局部作用域变少了
实际上,在做任何提炼前,我一般都会先移除局部变量
移除format变量
只有恰如其分地命名,才能彰显出将大函数分解成小函数的价值
但要一次把名取好并不容易,因此我会使用当下能想到最好的那个<br>如果稍后发现有更好的,我将毫不犹豫地换掉它<br>通常你需要花几分钟通读更多代码,才能发现最好的名称是什么
移除观众量积分之和
把与更新volumeCredits变量相关的代码都集中到一起,有利于"以查询取代临时变量(178)"手法的施展
软件的性能通常只与一小部分相关,改变其他的部分往往对总体性能贡献甚微
有些,一些重构手法也会显著影响性能,我通常也不管它,继续重构,因为有一份结构良好的代码,回头调优也容易得多
重构后再花时间调化,调优时可能会回退早先做的一些重构,但更多时间,因为重构可使用更高效的调优方案。
特别是与复杂代码打交道时,细小的步子是快速前进的关键
1.5 进展:大量嵌套函数
所一整块代码拆解成大嵌套函数可以使代码结构更清晰
1.6 拆分计算阶段与格式化阶段
重构早期的一般步骤:拆解代码块,方便更好的理解它
把复杂的代码块分解为更小的单元,与好的命名一样重要
1.7 进展:分离到两个文件(和两个阶段)
将代码抽取到函数会带来额外包装成本:代码行数可能变多,但重构也提高了代码可读性
虽说言以简为贵,但可演化的软件却以明确为贵
编程时,需遵循营地法则:保证你离开时的代码库一定比你来时更健康
1.8 按类型重组计算过程
1.9 进展:使用多态计算器来提供数据
1.10 总结
第2章 重构的原则
2.1 何谓重构
重构(名词):对软件内部结构的一种调整,不改变软件可观察行为前提下,提高可理解性,降低修改成本
重构(动词):使用一系列重构手法,不改变“软件可观察行为”前提下,调整其结构
重构的关键在于运用大量微小且保持软件行为的步骤,一步步达成大规模修改
结构调整(Restructuring)和重构(Refactoring)是有区分的
若有人说他们的代码在重构时有一两天不可用,他们不是在重构!因为重构是行为保持的!
2.2 两顶帽子
Kent Beck提出了“两顶帽子”的比喻:添加新功能 & 重构
添加新功能时:不应该修改即有代码;通过添加测试并让测试正常运行
重构时:不能再添加功能,只调整代码结构;若非绝对必要,不应该添加任何测试
无论何时都应该清楚自己戴的是哪一顶帽子,且明白不同的帽子对编程状态提出的不同要求
2.3 为何重构
使软件更容易理解
代码不仅是给计算机读的,最重要的是代码也是给人读的
帮助找到bug
Kent Beck经常形容自己:我不是一个特别好的程序员,我只是一个有着一些特别好的习惯的还不错的程序员
提高编程速度
添加新功能时,内部质量良好的软件让我可以容易找到在哪修改,如何修改
良好的模块划分使我只需理解代码库一小部分即可做出修改
若代码清晰,引入bug的可能会变小,即使引入了bug,调试也容易很多
设计耐久性假说
2.4 何时重构
预备性重构:让添加新功能更容易
重构的最佳时机就在添加新功能之前
帮助理解的重构:使代码更易懂
有计划的重构和见机行事的重构
长期重构
怎么对经理说
何时不应该重构
2.5 重构的挑战
延缓新功能开发
重构的唯一目的就是让我们开发更快,用更少的工作量创造最大的价值
代码所有权
代码所有权的边界会妨碍重构
推荐团队代码所有制,每个成员都可修改此团队拥有的代码
跨团队代码所有制,有些团队鼓励类似于开源的模型:pull request
分支(版本控制)
合并与集成是两回事
合并(Merge):从主干合并到我的分支,这是一个单向代码移动,我的分支变更了,但主干没有
集成(Integrate):双向过程,不仅把主干pull到我的分支,还要把我分支的修改push回主干
好的实践是:持续集成(Continuous Integration, CI),也叫"基于主干开发"(Trunk-Based Development)
使用CI时,每个团队成员每天至少向主干集成一次
需要有相关实践确保主干随时处于健康状态,要学会:
大功能拆分成小块
使用特性开关(feature toggle)
CI可以降低分支合并难度,但最重要的原因还是CI与重构能良好配合,所以Kent Beck在极限编程中同时包含了这两个实践
测试
不会改变程序可观察的行为,这是重构的一个重要特征
想要重构,先要有可以"自测试的代码":测试套件运行速度要快(否则我不愿意频繁运行它),用于"快速发现错误"(重构可能引起错误)
自测试代码与持续集成紧密相关,我们仰赖CI及时捕获分支集成时的语义冲突。自测试代码是极限编程另一重要组成部分,也是CI的关键环节
遗留代码
此问题无简单解决之法,我能给出的最好建议是买一本《修改代码的艺术》,照书中指导来做。别担心书太老,尽管已出版十多年,其中建议仍管用
一言以蔽之,先找到程序的接缝,在接缝处插入测试,如此将系统置于测试覆盖之下
不建议尝试一鼓作气把复杂混乱的遗留代码重构成漂亮的代码,每次一小块,一点点来
数据库
Pramod Sadalage发展出一套渐进式数据库设计和数据库重构的办法,已被广泛使用
精要:借助数据迁移脚本,将DB结构的修改与代码相结合
与通常的重构一样,数据库重构的关键点也是小步修改且每次修改都应该完整
并行修改(Parallel Change)
先加一个新字段,数据同时写入新旧字段,调用处也修改使用新字段
暂停一小段时间,观察若无bug,再删除无人使用的旧字段
2.6 重构、架构和YAGNI
重构极大的改变了人们考虑软件架构的方式
错误:写代码前,先完成软件设计和架构,一旦代码写出来,架构就固定了,逐渐腐败
正确:有了重构技术,就有能力大幅度修改生产环境运行多年的软件架构
三大软件原则
1. 不做重复的事 | DRY(Don't Repeat Yourself)
2. 保持简单直接 | KISS(Keep it Simple Stupid)
3. 你不需要它 | YAGNI(You Ain’t Gonna Need It)
YAGN并非"不做架构性思考"
YAGNI是架构、设计与开过过程融合的一种工作方式,此工作方式需有重构为基础才可靠
2.7 重构与软件开发过程
重构的第一块基石是自测试代码,自测试代码也是持续集成的关键环节
三大实践:自测试代码,持续集成,重构
有这三大核心实践打下的基础,才谈得上运用敏捷思想的其他部分
2.8 重构与性能
不赞成为了提高设计纯洁性而忽视性能,重构可能影响性能,但它也使性能优化更容易
"编写快速软件(快速指:性能好)"的秘密是:先写出可调优的软件,再调优
3种性能提升法
第1种:时间预算法 | 用于心律调节器之类高度重视性能的系统,但企业信息系统追求如此高性能就过分了
第2种:持续关注法 | 此法作用不大,因为90%的优化工作都是白费劲
第3种:发现热点,去除热点 | 使用度量工具监控程序运行,在较小粒度范围内定位性能问题并解决
2.9 重构起源何处
重构(refactoring)一词的真正起源已经无从知晓了
最早认识重构重要性的两人是Ward Cunningham和Kent Beck
Smalltalk的“编译-链接-执行”周期非常短,容易快速修改代码,特别适合重构
Ralph Johnson(GoF之一)是Smalltalk社区另一位领袖
Ralph Johnson的博士研究生学生Bill Opdyke的博士论文是重构领域第一部丰硕的研究成果
John Brant和Don Roberts 开发了第一个自动化重构工具:Refactoring Browser(重构浏览器)
重构的概念已被行业广泛接受,但"重构"一词也被滥用,也许只是做了无视行为保持的不严谨的结构调整
Martin Fowler写了《重构1》和《重构2》
2.10 自动化重构
IDE的Refactor功能
重构工具背后的原则
粗糙的文本操作:查找 / 替换
IDE既能处理文本,还能处理语法树:IDE比文本编辑器更先进的地方
静态类型语言(如:JAVA)的重构可以完全安全,完全自动
2.11 延展阅读
有大量关于重构的材料已超本书范围
本书关注点是组织一本重构参考书,若需入门教材,推荐Bill Wake的《重构手册》,有很多重构练习
Josh Kerievsky的《重构与模式》紧密连接了“重构”和“软件模式”这两个世界
本书聚集通用编程语言重构技巧,还有一些专门领域的重构
《数据库重构》| Scott Ambler & Pramod Sadalage
《重构HTML》| Elliotte Rusty Harold
《修改代码的艺术》| Michael Feathers
主要讨论如何在缺乏测试覆盖的老旧代码库上开展重构
重构网站:https://refactoring.com
第3章 代码的坏味道
大牛说重构
仝键
其实这两招(提炼函数和改名)是最难的,重构界的蛋炒饭
3.1 神秘命名(Mysterious Name)
重构手法
改变函数声明(124)
变量改名(137)
字段改名(244)
好名字是整洁代码最重要一环
整洁代码(clean code)最重要的一环就是好的名字:用于清晰表达功能和用法
好名字节省猜谜时间
为程序元素改名,好的名字可以节省未来用在猜谜上的大把时间
坏名字潜藏设计问题
想不出一个好名字,说明背后可能潜藏更深的设计问题
改名是最常用重构手法
然而,很遗憾,命名是编程中最难的两件事之一:因此,改名是最常用重构手法
3.2 重复代码(Duplicated Code)
重构手法
提炼函数(106)
移动语句(223)
函数上移(350)
3.3 过长函数(Long Function)
重构手法
提炼函数(106)
99%的场合,用此招把函数变短
拆分循环(227)
条件表达式
分解条件表达式(260)
以多态取代条件表达式(272)
大量参数和临时变量阻碍"提炼函数"
以查询取代临时变量(178)
引入参数对象(140)
保持对象完整(319)
以命令取代函数(337)
短函数
活得最长最好
更好的阐释力,更易于分享,更多的选择
早期编程语言中子程序调用要额外开销,现代编程语言没这个问题了
函数命名原则
小函数易于理解的关键在于好名字,见名知义,无需关心其实现代码
以其用途,而非实现手法进行命名
哪怕函数调用动作比函数自身还长,若函数名能解释其用途,也要毫不犹豫地用长名字<br>关键不在函数长度,在于函数“做什么”和“如何做”之间的语义距离
何处提炼
需要写注释的地方
如果要写注释,就应该提炼到一个独立函数中,哪怕只有一行代码;用命名替代注释
有条件表达式和循环的地方