文章目录
- 2 重构原则
- 3 代码的坏味道
- 3.1 Duplicated Code(重复代码)
- 3.2 Long Method(过长函数)
- 3.3 Large Class(过大的类)
- 3.4 Long Parameter List(过长参数列)
- 3.5 Divergent Change(发散式变化)
- 3.6 Shotgun Surgery(霰弹式修改)
- 3.7 Feature Envy(依恋情结)
- 3.8 Data Clumps(数据泥团)
- 3.9 Primitive Obsession(基本类型偏执)
- 3.10 Switch Statements(switch惊悚现身)
- 3.11 Parallel Inheritance Hierarchies(平行继承体系)
- 3.12 Lazy Class(冗赘类)
- 3.13 Speculative Generality(夸夸其谈未来性)
- 3.14 Temparary Field(令人迷惑的暂时字段)
- 3.15 Message Chains(过度耦合的消息链)
- 3.16 Middle Man(中间人)
- 3.17 Inappropriate Intimacy(狎昵关系)
2 重构原则
2.1 何谓重构
重构是什么:前提是不改变软件可观察行为,目的是提高代码可理解性,降低代码修改成本
重构的方法论关注两方面内容:1、如何做重构手法的选型;2、如何使用重构才不会出错de
软件开发人员的两顶帽子:添加新功能,以及重构。软件开发过程中可能会发现自己经常变换帽子,但无论何时都应该清楚自己戴的是哪一顶帽子。
2.2 为何重构
重构可以,并且应该用于以下几个目的:
- 重构改进软件设计:当人们只为短期目的,或是在完全理解整体设计之前就贸然修改代码,代码的结构会累积性流失,程序的设计会逐渐腐败变质。对于这一问题,其一应该在初始设计时为代码制定一套严格的、细粒度的开发框架(DSL领域建模语言),严格限制开发人员的代码习惯;其二应该在添加新功能的同时持续重构。
- 重构使软件更容易理解:程序不仅是与计算机交流的语言,也是与其他程序员,或者未来某个时间的自己交流的语言。代码易读的核心是“准确说出我所要的”,“把应该记住的东西写进程序里”;重构帮助理解难以理解的代码;
- 重构帮助找到bug:重构代码,深入理解代码的作为,并把新的理解反馈回去。重构能帮我们写出强健的代码;
- 重构提高编程速度:良好的设计是快速开发的根本。没有良好的设计,后续短时间内会进展迅速,但恶劣的设计很快会让速度慢下来(复杂系统),陷入不断调试bug的泥潭,难以添加新功能。
2.3 何时重构
重构不是一件应该特别拨出时间注意的事情,重构应该随时随地进行,不应该为了重构而重构,重构的目的应该是帮助自己把要做的事做好。
三次法则:第三次做类似的事,应该重构
添加功能时重构:添加功能时避免原有代码结构的流失
修补错误时重构:有bug出现说明代码不够清晰
复审代码时重构:代码或许对自己来说很清晰,对他人则不然。代码复审团队应保持精炼,往往一个复审者和一个原作者就可完成;设计复审则需要借助UML类图等工具,复审团队也可以更大。
程序有两面价值:“今天可以为你做什么”和“明天可以为你做什么”。大部分时候我们总是关注今天可以做什么,让程序能力更强,但这只是价值的一部分。如果总是为了求完成今天的任务而不择手段,是无法进行长期的开发工作的。
关于间接层:计算机科学是这样的一门科学,它相信所有问题都可以通过增加一个间接层来解决。
- 间接层的弊端:委托层数越来越多,程序越来越难以理解
- 间接层的价值:允许逻辑共享;分开解释意图和实现;隔离变化;封装条件逻辑
2.5 重构的难题
数据库:一般情况下,商用程序会与背后的数据库结构紧密耦合在一起。在非对象数据库中,可以在对象模型和数据库模型之间插入一个分割层
修改接口:如果重构手法改变了已发布接口,你必须维护新旧两个接口,尽量让旧接口调用新接口,旧函数调用新函数。为了不留下过多的旧接口,应该严格控制接口的发布。
仅通过重构难以解决一些设计缺陷
何时不该重构:1)程序的大部分功能都不能用时,应该重写而不是重构;2)临近项目交付时,不应重构。
2.6 重构与设计
重构使设计不必面面俱到。一般的实践是,设计一个简单系统,并通过重构使系统更加丰满。
教训:哪怕你完全了解系统,也请实际度量它的性能,不要臆测。臆测会让你学到一些东西,但十有八九你是错的。
2.7 重构与性能
虽然重构可能使软件运行更慢,但它也使软件的性能优化更容易。
3 代码的坏味道
“味道”:指某些特定的代码结构,这些结构明显具有重构的可能性。
3.1 Duplicated Code(重复代码)
同一个类的两个函数含有相同的表达式,可以采用抽取函数
提炼出重复的代码
两个互为兄弟的子类内含有相同的表达式,可以使用抽取函数
和函数上移
,将重复部分推入到超类中。如果代码之间只是类似并不完全相同,就需要先抽取函数
将相似部分和差异部分分隔开,再运用构造模板方法
。
如果有些函数用不同的算法做相同的事,应该选择其中较清晰的一个,并使用替换算法
将其他函数的算法替换掉。
如果两个毫不相干的类出现重复代码,应该考虑对其中的一个使用提炼类
,将重复代码提炼到一个独立类中,然后在另一个类中使用这个新类。需要决定这个函数放在哪里合适,放在哪里不会再在其他任何地方出现。
3.2 Long Method(过长函数)
面向对象的程序中往往有无穷无尽的委托,而没有太多的计算
- 函数越长越难理解
- 进程内函数调用开销一般可忽略
- 小函数更容易起一个好名字,更易于理解
每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途命名。即使只提取出一行,即使调用这个函数比实现函数本身代码还要多,只要函数名能解释其用途,就应该毫不犹豫的重构。
一般来说提取函数
足以处理过长函数
如果函数内有大量的临时变量和参数,仅仅使用提取函数
,会将大量参数和临时变量作为新函数的参数,导致可读性几乎没有提升。此时可以经常运用以查询取代临时变量
来消除临时元素,以引入参数对象
和保持对象完整
处理过长的参数列表。
如果仍有大量临时变量和参数,可以使用杀手锏以函数对象取代函数
。
确定提炼代码段的技巧:
- 寻找注释,注释说明这段代码需要额外的信息帮助理解,代码的用途和实现手法之间存在gap
- 条件表达式和循环。可以使用
分解条件表达式
来处理条件,可以将循环和内部的代码提炼到单独函数
3.3 Large Class(过大的类)
太多实例变量:运用提炼类
和提炼子类
,将彼此相关的几个变量(往往有着相同的前缀或后缀)放到一起
太多代码:先确定客户端如何使用这些代码,再运用提炼接口
为每一种方式创建一个接口,可以帮助分解过大的类。将尽可能多的逻辑在类的内部闭环。
大类是GUI类:可能需要将数据和行为移到一个独立的领域对象中,可能在两边都保留一些重复数据,并用机制保证两处数据同步。使用复制“被监视数据”
。
3.4 Long Parameter List(过长参数列)
面向对象较之面向过程的一个好处是,面向过程的函数上下文只能通过参数和全局变量传递,过多的全局变量和参数列表都会使程序变得晦涩难懂;面向对象的函数可以从宿主类中获取大多数信息,这使函数有更短的参数列表。
避免将一个对象传递给函数,而函数仅修改其中一两个字段的现象。函数应该仅知道它应该知道的东西。
- 如果可以从已有对象(包括其他参数、宿主类字段)中通过查询、计算等方式获取到某个参数,应该
以函数取代参数
- 如果参数中的一堆数据同属于一个对象,应该
保持对象完整
,传入整个对象而非零碎的字段 - 如果一堆数据实际上有关联,但缺乏合理的对象归属,应该
引入参数对象
- 例外情况,不想造成被调用对象与较大对象间的依赖关系,可以将数据从对象中拆解出来单独作为参数
3.5 Divergent Change(发散式变化)
针对某一外界变化的所有相应修改都应该发生在单一类中,而这个新类内的所有内容都应该反应此变化。
一个类是否可能发生发散式变化,在设计阶段往往是难以分辨的,只有在真正发生变化时才能意识到,然后你就需要重构,运用提炼类
把一个原因造成的变化提炼到另一个类中。
3.6 Shotgun Surgery(霰弹式修改)
与发散式变化类似,发散式变化
指的是一个类受多种变化的影响,霰弹式修改
指的是一个变化需要在许多不同的类中做出许多小的修改。坏处是不仅很难找到它们,而且很容易忘记某个重要的修改。两种情况都是为了使“外界变化”与“需要修改的类”趋于一一对应。
应该使用搬移函数
和搬移变量
将所有需要修改的代码放到同一个类,通常可以运用将类内敛化
把一系列相关行为放进同一个类。
3.7 Feature Envy(依恋情结)
对象技术的全部要点在于,将数据和对数据的操作行为包装在一起。
如果一个函数使用了大量另一个类的取值方法,应该使用搬移函数
移动到那个它真正依赖的类中。有时候需要用提炼函数
将有依恋情结的代码块单独提炼出来再进行搬移。现实的例子中一个函数往往会依赖来自多个类的数据,难以确定它应该放到什么地方。此时应该判断一下这个函数依赖哪个类更多一些,或者使用提炼方法
将函数分块,分别放到依恋的类中。
存在一些特例,如策略模式、访问者模式。这些模式的原则是将总是一起变化的东西放在一块儿,避免发散式变化。这种情况下的依恋情结是可以接受的。
3.8 Data Clumps(数据泥团)
总是绑定在一起出现的数据应该有他们自己的新对象。使用提炼类
。对于经常一起出现的函数参数,可以使用引入参数对象
和保持对象完整
将参数列缩短。只要能用新对象取代两个以上的字段,这种重构就是值得的。
评价是否应该引入一个新对象的评判方法是:删掉众多数据中的一项,其他数据是否因此失去意义。如果失去了意义,说明它们之间存在依赖关系,应该给它们新建一个新对象。新建一个对象的好处还包括,后续可以将设计这个对象的数据操作都放到对象中,帮更多流浪的函数回家。
3.9 Primitive Obsession(基本类型偏执)
对象的一个价值在于,模糊了基本类型和较大结构体之间的界限。可以尝试在小任务中使用小对象,而非总是使用基本类型(毕竟基本类型不能添加方法)。
- 如果想要替换的数据是类型码,可以使用
以类取代类型码
。如果后续有针对类型码的条件分支,可以使用以子类取代类型码
或以State/Strategy取代类型码
加以处理 - 如果有一组总是被放在一起的字段,应该运用
提炼类
。类似数据泥团
- 如果发现正从数组中挑选数据,可运用
以对象取代数组
3.10 Switch Statements(switch惊悚现身)
面向对象程序的一个明显特征就是少用switch。大型的switch可以用多态的特性轻松替代
大多数时候,switch都可以考虑使用多态来解决。switch语句常常通过类型码进行选择,推荐使用提炼方法
将switch中的语句提炼到一个独立函数,再以搬移函数
将它搬移到需要多态性的那个类中。此时便可以使用以类/子类/state/strategy替代类型码
构造继承结构。一旦完成继承结构,就可以使用以多态取代条件表达式
了。
如果仅想选择而不像对数据进行改动,则不必麻烦用多态。可以使用以明确函数渠道参数
,如果有一个选项是NULL,建议使用引入NULL对象
。
3.11 Parallel Inheritance Hierarchies(平行继承体系)
霰弹式修改的特殊情况,每当为一个类增加一个子类,也必须为另一个类增加一个子类。通常表现为两个继承体系的类名称前缀完全相同。
一般性策略是:让继承体系A的实例引用继承体系B的实例,消除重复。更进一步地,可以使用搬移函数
和搬移字段
完全消除一个继承体系。
3.12 Lazy Class(冗赘类)
每创建一个类,都要花费额外的代码维护代价。如果一个类阿紫重构过程中功能缩水,或者在实现/运行过程中发现类并未起到预期的作用,那么这个类就属于冗赘类,应该删除。
删除手法可以用折叠继承体系
,对于几乎没有用的组件,可以用将类内联化
。
3.13 Speculative Generality(夸夸其谈未来性)
当考虑到总有一天需要做某件事,并因而企图用各种各样的狗子和特殊情况来处理一些非必要的事情时,这种坏味道就出现了。这样做的结果往往是使系统更复杂更难维护,而预想中的特殊情况在软件生命周期内可能永远都不会发生。
- 如果某个抽象类没有太大作用,请运用
折叠继承体系
- 不必要的委托可运用
内联类
- 函数的某些参数未用到,请运用
移除参数
- 函数名称带有多余的抽象意味,请运用
重命名函数
- 如果一个类/函数的唯一调用点是测试用例(这个类/函数不属于测试框架),应该连同测试用例和类/函数一起删掉
3.14 Temparary Field(令人迷惑的暂时字段)
表现为对象内的某个字段仅为某种特定情况而设。面向对象思想中,通常认为对象中的字段和对象本身是相同生命周期的,对象在所有时候都需要它的所有字段。
运用提炼类
,将这些孤儿字段和相关的操作放到新的类。或许会用到引入NULL对象
处理变量不合法的问题。
这种坏味道常出现在一个类中有一个复杂算法,需要多个变量。实现者不想让这些变量都作为参数引入,就出现了仅有这个算法才会用到的孤儿字段。此时应该运用提炼类
将这些变量和这个复杂算法提炼到一个独立的类中,提炼后的新对象将是一个函数对象。
3.15 Message Chains(过度耦合的消息链)
表现为一个对象请求另一个对象,再向后者请求另一个对象…这种现象的坏处是,一旦对象间关系发生一处变化,这个消息链就变得无效。
可以运用隐藏委托关系
,重构消息链上的任何一个对象,但是容易出现大量中间人
更好的选择是,先观察消息链的终点对象是做什么的,用提炼方法
将用到终点对象的代码提炼到一个独立函数中,再运用搬移方法
将函数推入消息链。如果消息链的中间环节还有其他操作,也可以递归地提炼、推入函数。
3.16 Middle Man(中间人)
如果一个类接口有一般的函数都委托给其他类,那么说明过度运用了委托,这个类称为中间人。
请运用移除中间人
,和真正负责的对象打交道。如果这种拉通类只有少数几个,可以用于内联方法
将它们放到调用端。如果这些拉通类还有其他行为,可以运用以继承取代委托
将它变成实责对象的子类,这样既可以扩展原对象的行为,又不必负担多数委托任务。
3.17 Inappropriate Intimacy(狎昵关系)
类之间有太多的行为去探知对方的私有字段,则需要采取措施
- 采用
搬移函数
或者搬移字段
划清界限 - 运用
将双向关联改为单向关联
- 运用
提炼类
将两者共同点提炼到安全地点 - 运用
隐藏委托关系
继承往往是过度亲密的高发区,因为子类对超类的了解总是超过超类的主观意愿。如果觉得子类可以不这么依赖超类了,可以运用以委托代替继承