概述
目录
- 《重构-改善既有代码的设计》
- 第一章
- 第二章
- 第三章 代码的坏味道
- 第六章 重新组织函数
《重构-改善既有代码的设计》
这本书有些“年纪”了,按理说it界的书都是读新不读旧。但它有点特别,其中的关于重构和面向对象的思想我觉得放到现在也是不过时的。好多时候我们考察一个程序员,都是看他懂多少东西,知道多少概念,做过什么项目,但其实一个程序员最关键的素质是要写出好的代码。好的代码不是一天炼成,往往都要经过大量重构迭代之后才能成型,这本书就是讲怎么去重构代码的。在一线coding的程序员估计对书中的很多观点感同身受。我看完后也受益颇多,想把其中的一些心得记录下来,不一定是原文翻译,其中可能会有一些我自己的感想的“私货”。接下来就开始吧。
第一章
- 更改变量名是绝对值得的行为,好的代码应该清楚表达出自己的功能,变量名是代码清晰的关键。任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员 。
- 绝大多数情况下,函数应该放在它所使用的数据的所属对象内。
- 重构的前提是需要有测试工具和测试用例,每次重构一小部分代码就进行测试,保证在不改变代码行为的前提下重构。
- 不要对另一个对象的属性用switch,如果要用switch,也应该在自己的属性上用,这样可以解耦,别的对象的修改也影响不到你。
- 影片可能会根据不同的类型有不同的收费规则,所以影片可以根据类型有不同的子类,但是有个问题,影片如果换了类型,但是class是不能换的,所以可以考虑影片没有子类,但是影片包含一个Price的类,这个类可以根据不同类型有不同子类。Movie有一个Price类的引用,不同的类型对应不同的Price子类,调用Price类的方法来计算收费。
第二章
- 两顶帽子:添加新功能和重构。首先尝试添加新功能,然后意识到如果把程序结构改一下,添加新功能会方便很多,于是你换顶帽子,做一会重构。然后把帽子换回来,继续添加新功能,如此循环往复。
- 完成同样功能,设计不好的代码往往需要更多的代码量,这是因为不好的代码在不同的地方使用相同的语句在做同样的事情,当你修改了一处忘了另一处就容易出bug,所以消除重复代码,保证同样的逻辑只出现一次,这正是优秀设计的根本。
- 重构是软件更容易理解:代码的第一个读者是计算机,你必须写出让计算机能理解和执行的代码。代码的第二读者是未来的某个开发者(很有可能是你自己),你需要写出他能理解的代码,这样才不会只修改某段代码就花上一星期。
- 重构帮你提升开发速度。良好的设计是快速开发的根本。如果没有良好的设计,可能一开始进展的很快,但是马上就会被大量bug淹没,导致你花很多时间去debug,打上一个又一个补丁,新功能又需要更多代码才能实现,恶性循环。
- 重构的时机:不要安排固定的时间进行重构,而是开发阶段随时随地进行重构。
三次法则:第一次做某件事时尽管去做,第二次做类似的事产生反感,但还是可以去做,第三次再做类似的事情,你就必须要重构了。
添加新功能的时候是最常见的重构的时机,重构可以让开发者理解需要修改的代码,同时也让增加新特性更轻松一些。调试的时候也经常重构,一是可以增加可读性,二是能够帮忙找出bug。代码review的时候也可以进行重构,它能让你把代码看的更清楚,提出更多的有建设性的建议。
程序有两面价值,今天能为你做什么和明天能为你做什么。很多时候我们只关心今天能为你做什么,修复bug添加功能,其实很少关注明天。当明天发现今天的设计不能满足要求,那我们就开始重构吧。
重构也会有一些领域很难推进:
1.数据库。数据库的结构很难改变,一旦改变意味着你不得不迁移数据。
2.修改接口。如果接口的调用都是可控的,那对接口的重构很简单。但是接口如果是已发布接口,那你就必须维护新旧两套接口。尽量用旧接口调用新接口而不是复制代码。在一个功能包定义一个异常基类,这样就可以避免增加异常类型导致编译错误。
何时不该重构:重写的一个信号是:当前的代码根本不能正常运作。而重构的前提是代码能在大部分情况下正常运作。项目的后期也不该重构,因为很可能会赶不上项目的deadline。项目的重构就像债务,很多公司需要借债来使项目高效地运转,但借债会付利息,低质量代码的维护和扩展的额外成本就是利息。你可以承受一部分利息,但利息太高整个项目就垮了。如果最后你没有足够的时间,通常说明早就该进行重构。 - 早期的软件开发特别强调设计,期望预先设计出一个足够完美,异常灵活的解决方案,希望它能承受住所有我能预见的需求变化,问题是构造这么一套方案,所需成本难以估算,而且这个方案也肯定比简单的解决方案要复杂许多。系统的变化可能出现在任何地方,如果在所有可能变化的地方都建立灵活性,复杂度和维护难度都会大大提高。如果最后发现这些灵活性毫无必要,这才是最大的失败。有些灵活性你花了大量的时间,最后确发现派不上用场。有了重构,就不需要进行重度的预先设计,你可以先考虑下把一个简单的方案重构成灵活的方案有多大难度,如果是相当容易,那么就实现目前的简单方案就行了。重构可以带来更简单的设计,同时又不损失灵活性,也减轻了设计压力。有了重构,当下你可以只管构建可运行的最简化系统,至于灵活而复杂的设计,多数时候你不会需要,真需要的时候,可以通过重构来实现。
重构的时候,为了让软件易于理解,常会做出一些是程序运行变慢的修改,但换个角度说,重构也使得软件的性能优化更容易。性能的问题经常出现在小部分代码上,结构良好的代码在你进行性能分析的时候有更细的力度,性能的调整也比较容易。
第三章 代码的坏味道
-
坏味道首当其冲的就是Duplicated Code,idea现在都会对duplicate code默认给出警告。最单纯的duplicate code就是两个函数有相同的表达式或逻辑代码,这时就需要提炼出重复的代码,然后两个函数都调用被提炼出来的代码。如果是两个兄弟子类出现了duplicate code,可以考虑提炼出来放入超类中。如果两个毫不相干的类有duplicate code,那么把重复代码抽出来成为一个独立类,由两个类引用它,或者放到其中一个类,另一个类引用,由你自己觉得它最适合放的位置。
-
Long Method
程序越长越难以理解,小函数容易理解的真正关键在于一个好名字。如果读者可以通过名字了解函数作用,根本不必去看实现逻辑。我们可以遵循这条原则:但感觉需要以注释来说明什么的时候,我们就把需要说明的东西写进一个独立函数,并以其用途命名,甚至一行代码也可以这么做。大部分场合把函数变小,只需找到函数中适合集中在一起的部分,提炼出来形成一个新函数。如果有大量的参数和临时变量,会导致可读性很差。此时可以消除这些临时元素。如何确定该提炼哪一段代码?一个技巧是寻找注释。因为一般注释出现的时候,说明下面这段代码的用途和实现手法之间是有距离的,怕读者看不懂,所以加上注释解释下面的代码要做什么。那么你就可以把代码替换成一个函数,并在注释的基础上给函数命名。就算只有一行代码,如果它需要以注释来说明,那也值得提炼成一个函数。条件表达式和循环常常也是提炼的信号。你应该将循环和其内的代码提炼到一个独立函数中。 -
Large Class
单个类如果做太多事情,往往就会出现太多实例变量。我们可以把几个相关变量一起提炼到新类,新类可以是子类或者原来的类引用这个新类。大的class一般也有很多重复代码,最简单的解决方案就是把大函数拆解成若干小函数,代码量会下降很多。 -
过长的参数列表
太长的参数列表难以理解,而且往往你后期又会增加它的长度。如果参数能从对象获取,那就尽量从对象获取,如果某些参数不属于任何对象,那么可以制造一个参数对象,把这些参数都放进去。但有一点你要考虑到,这么做会增加类之间的依赖关系,所以有的时候把数据从对象中拆解处理单独作为参数也是合理的。两种方法需要你自己来评估优劣。 -
发散式变化
有的类因为不同原因在不同方向上发生变化被称为发散式变化(Divergent Change)。比如一个类,新加入数据库要修改3个函数,新出现一个工具要修改4个函数,那就意味着这个对象分成两个会比较好。针对某一个外界变化的所有相应修改,都只应该发生在单一类中。(对应设计模式里的单一职责原则) -
散弹式修改
跟5类似,表示如果遇到某种变化,你必须在许多不同的类做出许多小修改。这种情况你应该把需要修改的方法和变量放进同一个类,如果这个类不存在,那就新建一个。5和6的目的都是让外界变化和需要修改的类趋于一一对应。 -
依恋情节
函数对某个类的兴趣高过对自己所处的类的兴趣。经常会有某个函数为了计算出结果,从另一个对象那调用非常多的取值函数。那么解决方案显而易见:把这个函数移到另一个类中。如果一个函数中只有部分代码出现这种依恋,那么就把那部分独立成一个函数再迁移。但也有例外,比如Strategy和Visitor模式,这两个模式的本质是避免发散式变化,所以把变化的部分抽成了一个类(设计模式的变与不变原则)。所以到底用哪种方法取决于一个更高层次的原则:将总是一起变化的东西放在一起。不管是消除依恋情节还是使用Strategy和Visitor模式,原则都是保持变化只在一地发生。Strategy和Visitor模式让你可以轻松修改函数行为,因为它们将需要被overwrite的行为隔离开来了,但是也付出了多一层间接性的代价。 -
数据泥团
你经常可以在很多地方看到相同的数据:两个类中相同的字段,函数中相同的参数。这些一起出现的数据应该拥有属于他们自己的对象。这么做的好处是可以将很多参数列缩短,简化函数调用。一个好的判断方法:删掉众多数据中的一项,其他数据有没有因而失去意义,如果是,那你应该为它们产生一个新对象。一旦拥有新对象之后,你就能接着优化,比如把一些函数也移到新的类。不必太久,所有类都将在它们的小小社会中充分发挥价值。 -
基本类型偏执
对象技术的新手通常不愿意使用小对象,比如结合数值和币种的money类,有起始值和结束值构成的range类。可以尝试着把它们组成一个小类,这样就有机会把修改函数也放入这个小类。 -
switch
可以考虑用多态来替换switch -
平行继承体系
当你为某个类增加一个之类,必须也为另一个类增加一个之类。一般解决方案是:让一个继承体系的实例引用另一个继承体系的实例。(桥接模式) -
Lazy Class
如果某个类不再需要了,可以删除它或者Inline Class去掉。 -
夸夸其谈未来性
有时候会为了未来的某种需求设计了很多类,如果实际用不到,就把这些抽象的设计去掉。如果函数和类的唯一用户是测试用例,说明是有问题的。 -
临时字段
如果类中有一个复杂算法,需要好几个变量,由于实现者不希望传递一长串参数,所以他把这些参数都放进字段中,但这些字段只在使用该算法时才有效,这时候你可以extrace class把这些变量和相关函数提炼到一个独立类中。 -
过度耦合的消息链
一长串的get***方法,客户端代码和查找过程中的代码结构紧密耦合,一旦对象间的关系发生变化,客户端就需要改。解决方案:hide delegate或者把使用最终对象的代码提炼到一个独立函数中,再把这个函数推入消息链。 -
middle man
人们可能过度运用委托。某个类有一半的函数都委托给其他类,这就是过度运用,这时应该remove middle man,直接和真正负责的对象打交道。如果这样的函数只有少数几个,可以用inline method把它们放进调用端。如果middle man还有其他行为,可以把它变成实责对象的子类,这样既可以扩展原对象的行为,又不必负担那么多的委托动作。 -
Inappropriate intimacy
有时两个类过于亲密,经常访问对方的private变量。这时可以move method和move field帮他们划清界限,也可以extract class把共同点提炼到一个安全的地方供两个类使用。继承往往造成过度亲密,如果你觉得子类可以独自存在了,可以replace inheritance with delegation让它离开继承体系。 -
异曲同工的类
如果两个函数做同一件事情,却有不同的签名,请运用rename method重新命名,但这往往不够,还需要反复运用move method将某些行为移入类,直到两者的协议(可理解为对外接口)一致为止。如果你必须重复而冗余地移入代码,或许可以extract superclass -
不完美的库类
库往往构造的不够好,而且不能让我妈修改其中的类完成我妈希望完成的工作。如果你只想修改库类的一两个函数,可以Introduce foreign method,如果要添加一大堆额外行为,就用Introduce Local extension。 -
data class
只拥有字段和访问字段的方法的类。对这些类,要注意字段的封装,容器类的字段要检查它们是不是得到了恰当的封装,不该被修改的类把set方法去掉。尝试把get和set方法的调用代码搬移到data class,这样你就可以用hide method把这些函数隐藏起来。 -
被拒绝的遗赠
如果子类复用了超类的行为,却又不愿意支持超类的接口,坏味道就会变得浓烈。即使不愿意继承接口,也不要胡乱修改继承体系,应该用replace inheritance with delegation来达到目的。 -
过多的注释
comments可以帮我们找到各种坏味道,找到坏味道并用重构把坏味道去除之后,我们会发现注释已经变得多余了,因为代码已经说明了一切。如果你需要注释来解释一块代码做了什么,用extract method。如果还是需要注释来解释方法的行为,试试rename method;如果你需要注释来说明系统的需求规格,试试introduce assertion。除了记述将来的打算之外,注释还可以用来标记你并无十足把握的区域,你可以在注释里写下为什么做某某事。这可以帮助将来的修改者。
第六章 重新组织函数
- extract method
如果每个函数的粒度都很小,那么函数被复用的机会就更大,而且会使得高层函数读起来就像一系列注释,如果函数都是细粒度,那么修改也会更容易些。
当你能给小型函数很好地命名时,它们才能真正起作用。函数名的长度不是问题,关键在于函数名和函数本体之间的语义距离。如果提炼方法可以强化代码的清晰度,那就去做,就算函数名称比提炼出来的代码还长也无所谓。
提炼函数遇到的最麻烦的问题是目标函数修改了源函数的局部变量,此时目标函数需要把修改后的值作为函数的返回值,这样源函数就可以拿到这个返回值去赋值给局部变量,如果目标函数修改了多个局部变量,此时的选择最好是分成多个方法去提炼,也就是说分解成多个目标函数,每个目标函数返回一个局部变量的最新值。 - inline method
有时候你遇到某些函数,内部代码和函数名统一清晰易读,那你就应该去掉这个函数。间接性可能带来帮助,但是非必要的间接性总是让人不舒服。
还有一种情况是有一群组织不合理的函数,你可以将它们都内联到一个大型函数中,再从中提炼出合理的小型函数。
如果你在inline method的时候碰上了递归调用,多返回点,内联至另一个对象而该对象并无提供访问函数时,说明不应该使用这个重构方法。 - inline temp
多半是作为replace temp with query的一部分使用的,如果这个临时变量妨碍了其他的重构手法,比如extract method,就应该将它内联化。 - replace temp with query
临时变量的问题在于:它们是暂时的,而且只能在函数内使用。所以它们会让你写出更长的函数,因为这样你猜呢访问到临时变量。如果把临时变量替换为一个查询,那么一个类的所有函数杜能获得这份信息,能使你为这个类编写更清晰的代码。
我们常常使用临时变量保存循环中的累加信息。在这种情况下,整个循环都可以被提炼为一个独立函数,有时一个循环中会累加好几个值,那么就多创建几个函数,把所有临时变量都替换为查询。 - introduce explaining variable
在条件逻辑中,可以将每个条件子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的意义。在较长算法中,也可以用临时变量来解释每一步运算的意义。
用extract method一般也能得到一样的效果,但是,如果在extract method要花费更大工作量时,比如要处理一个拥有大量局部变量的算法,这种情况下就用本策略,然后再考虑下一步该怎么办。 - Split Temporary Variable 分解临时变量
有很多临时变量用于保存一段代码的运算结果。如果它们被赋值超过一次,说明承担了一个以上的责任,应该被替换为多个临时变量。 - remove assignments to parameters
在java中,不要对参数赋值,那会混淆了值传递和引用传递。(个人感觉java程序员必须要弄清楚值传递和引用传递的区别,这算最最基本的基本功了) - replace method with method object
有的函数因为包含很多局部变量,想要extract method非常困难,那么可以考虑新建一个类,把所有局部变量变成新类的field,然后把函数的代码复制过来,源函数改为调用新类的同名方法,这个新类的实例就称为method object,新类里面就可以做extract method了。 - substitute algorithm
有时你会发现在原先的做法之外,还有更简单的解决方案,此时你就可以考虑替换掉原来的算法。不过在进行该重构前,确认你对原算法非常了解。如果测试结果不同于原先,以旧算法为比较参照标准。
最后
以上就是野性冬瓜为你收集整理的重构-改善既有代码的设计 读书心得(一)《重构-改善既有代码的设计》的全部内容,希望文章能够帮你解决重构-改善既有代码的设计 读书心得(一)《重构-改善既有代码的设计》所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复