02重构的原则
2.1何为重构
重构的定义
使用一系列的手法,对软件内部结构的一种调整,目的是:
在不改变软件可观察行为的前提下,
提高可理解性,
减低修改成本(时间成本、人力成本、质量成本);
重构的关键
运用大量微小且保持软件行为的步骤,一步步达成大规模的修改;
重构过程中,每一步改动后软件保持可用状态;
重构和优化的对比
***相同点***
都需要修改代码;
都不会改变程序的整体功能;
***不同点***
重构的目的
重构是为了让软件更容易理解、更容易修改;
可能使得程序运行更快,也可能使程序运行更慢;
优化的目的
只关心让程序运行更快;
最终的代码可能更难以理解和维护;
2.2两顶帽子
两顶帽子 from Kent.Beck
使用重构技术开发软件时,我把自己的行为分配给2种截然不同的行为:
添加新功能
重构
添加新功能
添加新功能时,我不应该修改既有代码,只管添加新功能;
通过添加测试并让测试运行正常,我可以衡量自己的工作进度;
重构
重构时,我就不能添加功能,只管调整代码的结构;
此时我不应该添加任何测试;
除非发现之前有遗漏的功能;
只在绝对必要时修改测试(处理接口变化);
2.3为何重构
重构改进软件的设计
程序的内部设计或者架构会逐渐腐败变质;
人们只为短期目的修改代码时,经常没有完全理解架构,于是代码逐渐失去了自己的结构;
代码结构的流失有累积效应,越难理解,越难保护,腐败的越快;
经常重构,有助于代码维持应有的形态;
改进设计的一个重要方向就是,
消除重复代码;
**确定所有的事物和行为在代码里只表述一次------这是优秀设计的根本**
重构使软件更容易理解;
对于代码,除了计算机之外,还有其他读者;
后续的读者,要读懂或修改代码,对比计算机,这些读者才是更重要的;
很多时候,这位后续的读者,便是我们自己;
重构,帮助我们更清晰的表达,程序逻辑;
重构帮助找到bug
对于代码的理解,可以帮助我们找到bug;
而对代码进行重构,
就可以深入理解代码的所作所为,
并立即把新的理解反应在代码中;
搞清楚代码结构的同时,可以验证自己的一些假设;
于是,bug无处可藏;
“**我不是一个特别好的程序员,我只是一个有着特别好的习惯的还不错的程序员**”---from Kent.Beck
重构提高编程速度
重构可以提高质量
改善设计、提升可读性、减少bug;
在短期内可能花费一些时间在重构上,
随着时间的推移和功能的负责,重构可以大大的提高开发速度;
**两种团队**
需要添加新功能时,内部质量良好的软件,可以很容易的找到在哪儿修改、如何修改;
良好的模块划分,使我只需要理解代码库的一小部分,就可以做出修改;
代码清晰,引入bug的概率就会变小;
即时引入了bug,调试定位也会容易的多;
理想情况下,质量高的代码,会逐渐演化成一个平台
在其上,可以很容易的构造与其领域相关的新功能;
这种现象称之为“**设计耐久性假说**”,
通过投入精力概设内部设计,增加软件的耐久性,从而可以更长时间的保持开发的快速;
2.4何时重构
三次法则
第一次做某件事,只管去做;
第二次做类似的事,会产生反感,但是无论如何,还是可以去做;
第三次,再做类似的事,你就应该重构;
即为:
**事不过三,三则重构**;
重构的6种方式
方式1,预备性重构,让添加新功能,更容易;
重构的最佳时机:
就在添加新功能前;
对代码结构做一些调整,添加新功能就会很容易;
如果不做重构,
就需要复制结构类似代码,违背只表述一次逻辑;
将来需要修改,就需要修改多处;
方式2,帮助理解的重构,是代码更易懂;
修改一两个变量名,让他们更清晰的表达意图;
将一个常函数,拆分成几个小函数,结构明了;
“**初步的重构,就像扫去窗上的尘埃,使我们得以看到窗外的风景**”---from Ralph Johnson
重构引领我们获得更高层次的理解;
缺少了这些细微的理解,就无法看到隐藏在混乱背后的危机;
方式3,捡垃圾式重构
确认代码中的垃圾:
函数迂回复杂、代码重复;
如果垃圾很容易重构,那就重构他;
如果时间和精力不允许,那就把它记下来,完成当下的任务后再去重构他;
**让营地比你来的时候更整洁**;
重构的妙处:
每个小步骤,都不会破坏代码;
即便一块垃圾在好几个月之后才终于处理干净,
即便每次清理垃圾并不完整;
代码也不会被破坏;
方式4,有计划的重构和见机行事的重构
肮脏的代码需要重构;
但漂亮的代码,也需要很多重构;
“**每次要修改时,首先令修改很容易(有时候,会很难),然后再进行这次容易的修改**”---from Kent Beck
有计划的重构
如果他对过去忽视了重构,那么需要花一些时间来优化代码库;
对于很多公司来说,这是一种常态;
在重构上花一个星期的时间,会在未来几个月发挥价值;
见机行事的重构
大部分的重构,应该是不起眼的,见机行事的行为;
开发过程中遇到问题,重构使得工作变得简单或者代码清晰;
将重构与添加新功能在版本控制中,分开添加;
可以各自独立的审阅和批准这些提交;
这个不是毋庸置疑的原则,当你感觉有价值时,就这样做;
方式5,长期重构;
大型重构需要花费较长时间;
比如替换一个正在使用的库;
比如把代码抽取出来给另外模块使用;
比如处理一大堆的混乱关系;
编码让一个团队专门做重构
有效策略:
让整个团队达成共识;
未来的一段时间,逐步解决问题;
策略优点:
重构不会破坏代码;
每次小改动,整个系统正常工作;
替换一个正在使用的库
先引入一层新的抽象;
使其兼容新旧两个库的接口;
一旦调用方已经完全改为使用这层抽象;
替换下边的库将会容易的多;
**Branch By Abstraction**
方式6,代码审核时重构
Dont know why
如何对经理说
坚持做正确的事;
何时不应该重构
如果看见一块凌乱的代码,
如果不需要修改它,那么久不需要重构;
只有当需要理解其工作原理时,对其重构才有价值;
如果重写比重构还容易,那就别重构了;
2.5重构的挑战
挑战1-延缓新功能开发
重构的唯一目的
就是让我们开发的更快;
用更少的工作量,创造更大的价值;
特殊情况
一个大规模的重构很有必要进行;
马上要添加的功能非常小;
先把功能添加上,再完成这次大规模的重构;
如果一个问题我已经见过;
我更倾向于重构它;
有时一块丑陋的代码,我得先看见它几次;
我才有劲头去重构它;
如果一块代码,我很少触碰或者它不经常带来麻烦,
我就不去重构它;
如果我还没想清楚如何优化代码,
我倾向于延迟重构;
或者先做些实验,测试下能否有所改进;
挑战2-代码所有权
重构的影响范围
重构会影响模块内部;
重构会影响模块与系统其它部分的关系;
修改函数名;
函数的调用,可能由另一支团队的代码;
函数的调用,可能由客户的代码调用;
代码所有权的边界,会妨碍重构;
函数改名,需要保持原来的函数声明;
使其把调用传递给新的函数;
这会让接口变得复杂,
这就是为了避免破坏使用者的系统不得不付出的代价;
我们可以把旧的接口标记为“不推荐使用”
让旧的接口逐步退休;
有些时候,旧的接口必须一直保留下去;
挑战3-主干-分支开发方式
常见的主干-分支开发方式
在分支上开发完整的功能;
直到功能可以发布到生产环境,才把代码合并会主干;
优点:
能够保持主干不受尚未完成的代码影响;
能够保留清晰的功能添加版本记录;
在某个功能出问题时,可以很容易的撤销修改;
缺点:
在分支上开发的越久,合并会主干的难度越大;
合并难度,随着时间,指数上升;
解决方法
持续集成(Continuous Integration);
每个团队开发成员,每天至少向主干集成一次;
避免了任何分支彼此差异太大,从而大大降低了合并的难度;
挑战4-测试
重构的重要特征
不会改变程序的可观察行为;
我们总是会犯错误,每个重构步骤都是微小的改变;
造成bug时,我们只需要检查最后一步的小修改;
如果找不到原因,我们可以回滚到版本控制中的最后一个可用版本;
快速发现错误
要做到这点,我们要有一整套的完备测试套件;
且,执行速度要快,否则没人愿意频繁的运行测试;
绝大多数情况下,
先要重构,先要有可以自测试的代码;
自测试代码
自测试的代码,要求很高;
团队在此投入时间和经理,绝对是物超所值的;
自测试代码,使得重构称为可能,而且添加新功能更加安全;
自测试代码,回答了“重构风险太大,可能引入bug”的问题;
如果没有自测试代码,这种担忧是完全合理的;
自动化重构
C语言上没什么实现;
挑战5-遗留代码
重构可以让我们很好的理解遗留系统;
“没有测试就加上测试”
很难完整执行;
没有简单的解决办法;
挑战6-数据库重构
解决数据库重构经常出问题的方法:
借助数据迁移脚本;
将数据库结构的修改和代码结合;
使得大规模的、涉及数据库的修改可以比较容易的开展;
数据库重构的关键
小步修改;
每次修改完整;
分布到多次生产发布来完成;
即便某次修改造成了问题,比较容易回滚;
2.6重构、架构、YAGNI
重构与架构
重构极大的改变了人们考虑软件架构的方式;
早起架构思维:
在任何人写代码之前,必须先要完成软件的设计和结构;
但是,只有真正使用了软件,看到软件对工作的影响,人们才会明白自己到底需要什么;
重构技术,使得即便运行了很多年的程序,也可以大规模的修改其架构;
重构对架构的最大影响:
通过重构,
能够得到一个设计良好的代码库;
使其能够 优雅 应对不断变化的需求;
YAGNI
You Aren't Going to Nead It,你不会需要它;
个人理解:
代码功能,不超前、不滞后、重构到最需要的状态;
2.7重构与软件开发过程
重构是否有效
与团队采用的其他软件开发实践紧密相关;
重构的基石
重构的第一块基石
自测试代码;
重构的第二块基石
每个团队的成员都要掌握重构技能;
能够在需要时展开重构,而不会影响他人的工作;
这就是我鼓励CI(持续集成)的原因
协同效应
自测试代码;
持续集成;
重构;
YAGNI;
重构是YAGNI的基础;
YAGNI使得重构易于开展;
2.8重构与性能
编写快速软件的秘密
除了对性能有严苛要求的实时系统;
先写出可调优的代码;
然后调优以求获得足够的速度;
三种编写快速软件的方法
方法1、最严苛的时间预算法
每个组件分配一定的时间和空间,严格遵循;
方法2、持续关注法;
程序开发,要求任何程序员,在任何时间、做任何事,都要设法保持系统高效能;
通常没有效果;
**经验教训**
哪怕你完全理解系统;
也请实际度量它的性能;
臆测会让你学到一些东西,但是臆测有时候不一定是对的;
**关于性能**
如果你对大多数程序进行分析,就会发现,
大半时间都耗费在一小半的代码上;
如果一视同仁的优化所有代码,
九成的优化是白费劲的;
因为被优化的代码很少被执行;
方法3、性能提升法;
利用关于性能的”九成“的统计数据,优化性能;
编写代码时,只注重构造良好的程序;
不对性能投以特别的关注;
直至性能调优阶段,再遵循特定的流程来调优程序性能;
利用一个度量工具监控程序的运行;
分析出程序中哪些地方大量的消耗时间和空间;
**重构可以帮助优化性能;**
构造良好的代码,让我们有比较充裕的时间进行先调整;
能够更快的添加新功能,有更多的时间用于性能调优;
构造良好的代码,在进行性能分析时有较细的粒度;
度量工具可以把我们带入范围较小的代码段;
从而性能调优也比较简单;
2.9重构的起源
重构,没有真正的起源;
2.10自动化重构
开发工具的支持;
Java