重构-改善既有代码的设计 读书心得(一)

本文是《重构-改善既有代码的设计》一书的读书心得,探讨了重构的重要性,强调了写出清晰易懂代码的价值。书中指出,重构需要在测试保障下进行,通过改变量名、提取函数等手段提升代码质量。文章分享了如何在添加新功能、调试、代码审查时进行重构,并讨论了重构的时机和避免重构的领域。作者提醒,注意代码的坏味道,如重复代码、过长函数、大类等,并提供了相应的解决策略。

《重构-改善既有代码的设计》

这本书有些“年纪”了,按理说it界的书都是读新不读旧。但它有点特别,其中的关于重构和面向对象的思想我觉得放到现在也是不过时的。好多时候我们考察一个程序员,都是看他懂多少东西,知道多少概念,做过什么项目,但其实一个程序员最关键的素质是要写出好的代码。好的代码不是一天炼成,往往都要经过大量重构迭代之后才能成型,这本书就是讲怎么去重构代码的。在一线coding的程序员估计对书中的很多观点感同身受。我看完后也受益颇多,想把其中的一些心得记录下来,不一定是原文翻译,其中可能会有一些我自己的感想的“私货”。接下来就开始吧。

第一章

  1. 更改变量名是绝对值得的行为,好的代码应该清楚表达出自己的功能,变量名是代码清晰的关键。任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员
  2. 绝大多数情况下,函数应该放在它所使用的数据的所属对象内。
  3. 重构的前提是需要有测试工具和测试用例,每次重构一小部分代码就进行测试,保证在不改变代码行为的前提下重构。
  4. 不要对另一个对象的属性用switch,如果要用switch,也应该在自己的属性上用,这样可以解耦,别的对象的修改也影响不到你。
  5. 影片可能会根据不同的类型有不同的收费规则,所以影片可以根据类型有不同的子类,但是有个问题,影片如果换了类型,但是class是不能换的,所以可以考虑影片没有子类,但是影片包含一个Price的类,这个类可以根据不同类型有不同子类。Movie有一个Price类的引用,不同的类型对应不同的Price子类,调用Price类的方法来计算收费。

第二章

  1. 两顶帽子:添加新功能和重构。首先尝试添加新功能,然后意识到如果把程序结构改一下,添加新功能会方便很多,于是你换顶帽子,做一会重构。然后把帽子换回来,继续添加新功能,如此循环往复。
  2. 完成同样功能,设计不好的代码往往需要更多的代码量,这是因为不好的代码在不同的地方使用相同的语句在做同样的事情,当你修改了一处忘了另一处就容易出bug,所以消除重复代码,保证同样的逻辑只出现一次,这正是优秀设计的根本。
  3. 重构是软件更容易理解:代码的第一个读者是计算机,你必须写出让计算机能理解和执行的代码。代码的第二读者是未来的某个开发者(很有可能是你自己),你需要写出他能理解的代码,这样才不会只修改某段代码就花上一星期。
  4. 重构帮你提升开发速度。良好的设计是快速开发的根本。如果没有良好的设计,可能一开始进展的很快,但是马上就会被大量bug淹没,导致你花很多时间去debug,打上一个又一个补丁,新功能又需要更多代码才能实现,恶性循环。
  5. 重构的时机:不要安排固定的时间进行重构,而是开发阶段随时随地进行重构。
    三次法则:第一次做某件事时尽管去做,第二次做类似的事产生反感,但还是可以去做,第三次再做类似的事情,你就必须要重构了。
    添加新功能的时候是最常见的重构的时机,重构可以让开发者理解需要修改的代码,同时也让增加新特性更轻松一些。调试的时候也经常重构,一是可以增加可读性,二是能够帮忙找出bug。代码review的时候也可以进行重构,它能让你把代码看的更清楚,提出更多的有建设性的建议。
    程序有两面价值,今天能为你做什么和明天能为你做什么。很多时候我们只关心今天能为你做什么,修复bug添加功能,其实很少关注明天。当明天发现今天的设计不能满足要求,那我们就开始重构吧。
    重构也会有一些领域很难推进:
    1.数据库。数据库的结构很难改变,一旦改变意味着你不得不迁移数据。
    2.修改接口。如果接口的调用都是可控的,那对接口的重构很简单。但是接口如果是已发布接口,那你就必须维护新旧两套接口。尽量用旧接口调用新接口而不是复制代码。在一个功能包定义一个异常基类,这样就可以避免增加异常类型导致编译错误。
    何时不该重构:重写的一个信号是:当前的代码根本不能正常运作。而重构的前提是代码能在大部分情况下正常运作。项目的后期也不该重构,因为很可能会赶不上项目的deadline。项目的重构就像债务,很多公司需要借债来使项目高效地运转,但借债会付利息,低质量代码的维护和扩展的额外成本就是利息。你可以承受一部分利息,但利息太高整个项目就垮了。如果最后你没有足够的时间,通常说明早就该进行重构。
  6. 早期的软件开发特别强调设计,期望预先设计出一个足够完美,异常灵活的解决方案,希望它能承受住所有我能预见的需求变化,问题是构造这么一套方案,所需成本难以估算,而且这个方案也肯定比简单的解决方案要复杂许多。系统的变化可能出现在任何地方,如果在所有可能变化的地方都建立灵活性,复杂度和维护难度都会大大提高。如果最后发现这些灵活性毫无必要,这才是最大的失败。有些灵活性你花了大量的时间,最后确发现派不上用场。有了重构,就不需要进行重度的预先设计,你可以先考虑下把一个简单的方案重构成灵活的方案有多大难度,如果是相当容易,那么就实现目前的简单方案就行了。重构可以带来更简单的设计,同时又不损失灵活性,也减轻了设计压力。有了重构,当下你可以只管构建可运行的最简化系统,至于灵活而复杂的设计,多数时候你不会需要,真需要的时候,可以通过重构来实现。
    重构的时候,为了让软件易于理解,常会做出一些是程序运行变慢的修改,但换个角度说,重构也使得软件的性能优化更容易。性能的问题经常出现在小部分代码上,结构良好的代码在你进行性能分析的时候有更细的力度,性能的调整也比较容易。

第三章 代码的坏味道

  1. 坏味道首当其冲的就是Duplicated Code,idea现在都会对duplicate code默认给出警告。最单纯的duplicate code就是两个函数有相同的表达式或逻辑代码,这时就需要提炼出重复的代码,然后两个函数都调用被提炼出来的代码。如果是两个兄弟子类出现了duplicate code,可以考虑提炼出来放入超类中。如果两个毫不相干的类有duplicate code,那么把重复代码抽出来成为一个独立类,由两个类引用它,或者放到其中一个类,另一个类引用,由你自己觉得它最适合放的位置。

  2. Long Method
    程序越长越难以理解,小函数容易理解的真正关键在于一个好名字。如果读者可以通过名字了解函数作用,根本不必去看实现逻辑。我们可以遵循这条原则:但感觉需要以注释来说明什么的时候,我们就把需要说明的东西写进一个独立函数,并以其用途命名,甚至一行代码也可以这么做。大部分场合把函数变小,只需找到函数中适合集中在一起的部分,提炼出来形成一个新函数。如果有大量的参数和临时变量,会导致可读性很差。此时可以消除这些临时元素。如何确定该提炼哪一段代码?一个技巧是寻找注释。因为一般注释出现的时候,说明下面这段代码的用途和实现手法之间是有距离的,怕读者看不懂,所以加上注释解释下面的代码要做什么。那么你就可以把代码替换成一个函数,并在注释的基础上给函数命名。就算只有一行代码,如果它需要以注释来说明,那也值得提炼成一个函数。条件表达式和循环常常也是提炼的信号。你应该将循环和其内的代码提炼到一个独立函数中。

  3. Large Class
    单个类如果做太多事情,往往就会出现太多实例变量。我们可以把几个相关变量一起提炼到新类,新类可以是子类或者原来的类引用这个新类。大的class一般也有很多重复代码,最简单的解决方案就是把大函数拆解成若干小函数,代码量会下降很多。

  4. 过长的参数列表
    太长的参数列表难以理解,而且往往你后期又会增加它的长度。如果参数能从对象获取,那就尽量从对象获取,如果某些参数不属于任何对象,那么可以制造一个参数对象,把这些参数都放进去。但有一点你要考虑到,这么做会增加类之间的依赖关系,所以有的时候把数据从对象中拆解处理单独作为参数也是合理的。两种方法需要你自己来评估优劣。

  5. 发散式变化
    有的类因为不同原因在不同方向上发生变化被称为发散式变化(Divergent Change)。比如一个类,新加入数据库要修改3个函数,新出现一个工具要修改4个函数,那就意味着这个对象分成两个会比较好。针对某一个外界变化的所有相应修改,都只应该发生在单一类中。(对应设计模式里的单一职责原则)

  6. 散弹式修改
    跟5类似,表示如果遇到某种变化,你必须在许多不同的类做出许多小修改。这种情况你应该把需要修改的方法和变量放进同一个类,如果这个类不存在,那就新建一个。5和6的目的都是让外界变化和需要修改的类趋于一一对应。

  7. 依恋情节
    函数对某个类的兴趣高过对自己所处的类的兴趣。经常会有某个函数为了计算出结果,从另一个对象那调用非常多的取值函数。那么解决方案显而易见:把这个函数移到另一个类中。如果一个函数中只有部分代码出现这种依恋,那么就把那部分独立成一个函数再迁移。但也有例外,比如Strategy和Visitor模式,这两个模式的本质是避免发散式变化,所以把变化的部分抽成了一个类(设计模式的变与不变原则)。所以到底用哪种方法取决于一个更高层次的原则:将总是一起变化的东西放在一起。不管是消除依恋情节还是使用Strategy和Visitor模式,原则都是保持变化只在一地发生。Strategy和Visitor模式让你可以轻松修改函数行为,因为它们将需要被overwrite的行为隔离开来了,但是也付出了多一层间接性的代价。

  8. 数据泥团
    你经常可以在很多地方看到相同的数据:两个类中相同的字段,函数中相同的参数。这些一起出现的数据应该拥有属于他们自己的对象。这么做的好处是可以将很多参数列缩短,简化函数调用。一个好的判断方法:删掉众多数据中的一项,其他数据有没有因而失去意义,如果是,那你应该为它们产生一个新对象。一旦拥有新对象之后,你就能接着优化,比如把一些函数也移到新的类。不必太久,所有类都将在它们的小小社会中充分发挥价值。

  9. 基本类型偏执
    对象技术的新手通常不愿意使用小对象,比如结合数值和币种的money类,有起始值和结束值构成的range类。可以尝试着把它们组成一个小类,这样就有机会把修改函数也放入这个小类。

  10. switch
    可以考虑用多态来替换switch

  11. 平行继承体系
    当你为某个类增加一个之类,必须也为另一个类增加一个之类。一般解决方案是:让一个继承体系的实例引用另一个继承体系的实例。(桥接模式)

  12. Lazy Class
    如果某个类不再需要了,可以删除它或者Inline Class去掉。

  13. 夸夸其谈未来性
    有时候会为了未来的某种需求设计了很多类,如果实际用不到,就把这些抽象的设计去掉。如果函数和类的唯一用户是测试用例,说明是有问题的。

  14. 临时字段
    如果类中有一个复杂算法,需要好几个变量,由于实现者不希望传递一长串参数,所以他把这些参数都放进字段中,但这些字段只在使用该算法时才有效,这时候你可以extrace class把这些变量和相关函数提炼到一个独立类中。

  15. 过度耦合的消息链
    一长串的get***方法,客户端代码和查找过程中的代码结构紧密耦合,一旦对象间的关系发生变化,客户端就需要改。解决方案:hide delegate或者把使用最终对象的代码提炼到一个独立函数中,再把这个函数推入消息链。

  16. middle man
    人们可能过度运用委托。某个类有一半的函数都委托给其他类,这就是过度运用,这时应该remove middle man,直接和真正负责的对象打交道。如果这样的函数只有少数几个,可以用inline method把它们放进调用端。如果middle man还有其他行为,可以把它变成实责对象的子类,这样既可以扩展原对象的行为,又不必负担那么多的委托动作。

  17. Inappropriate intimacy
    有时两个类过于亲密,经常访问对方的private变量。这时可以move method和move field帮他们划清界限,也可以extract class把共同点提炼到一个安全的地方供两个类使用。继承往往造成过度亲密,如果你觉得子类可以独自存在了,可以replace inheritance with delegation让它离开继承体系。

  18. 异曲同工的类
    如果两个函数做同一件事情,却有不同的签名,请运用rename method重新命名,但这往往不够,还需要反复运用move method将某些行为移入类,直到两者的协议(可理解为对外接口)一致为止。如果你必须重复而冗余地移入代码,或许可以extract superclass

  19. 不完美的库类
    库往往构造的不够好,而且不能让我妈修改其中的类完成我妈希望完成的工作。如果你只想修改库类的一两个函数,可以Introduce foreign method,如果要添加一大堆额外行为,就用Introduce Local extension。

  20. data class
    只拥有字段和访问字段的方法的类。对这些类,要注意字段的封装,容器类的字段要检查它们是不是得到了恰当的封装,不该被修改的类把set方法去掉。尝试把get和set方法的调用代码搬移到data class,这样你就可以用hide method把这些函数隐藏起来。

  21. 被拒绝的遗赠
    如果子类复用了超类的行为,却又不愿意支持超类的接口,坏味道就会变得浓烈。即使不愿意继承接口,也不要胡乱修改继承体系,应该用replace inheritance with delegation来达到目的。

  22. 过多的注释
    comments可以帮我们找到各种坏味道,找到坏味道并用重构把坏味道去除之后,我们会发现注释已经变得多余了,因为代码已经说明了一切。如果你需要注释来解释一块代码做了什么,用extract method。如果还是需要注释来解释方法的行为,试试rename method;如果你需要注释来说明系统的需求规格,试试introduce assertion。除了记述将来的打算之外,注释还可以用来标记你并无十足把握的区域,你可以在注释里写下为什么做某某事。这可以帮助将来的修改者。

第六章 重新组织函数

  1. extract method
    在这里插入图片描述在这里插入图片描述
    如果每个函数的粒度都很小,那么函数被复用的机会就更大,而且会使得高层函数读起来就像一系列注释,如果函数都是细粒度,那么修改也会更容易些。
    当你能给小型函数很好地命名时,它们才能真正起作用。函数名的长度不是问题,关键在于函数名和函数本体之间的语义距离。如果提炼方法可以强化代码的清晰度,那就去做,就算函数名称比提炼出来的代码还长也无所谓。
    提炼函数遇到的最麻烦的问题是目标函数修改了源函数的局部变量,此时目标函数需要把修改后的值作为函数的返回值,这样源函数就可以拿到这个返回值去赋值给局部变量,如果目标函数修改了多个局部变量,此时的选择最好是分成多个方法去提炼,也就是说分解成多个目标函数,每个目标函数返回一个局部变量的最新值。
  2. inline method
    在这里插入图片描述
    在这里插入图片描述
    有时候你遇到某些函数,内部代码和函数名统一清晰易读,那你就应该去掉这个函数。间接性可能带来帮助,但是非必要的间接性总是让人不舒服。
    还有一种情况是有一群组织不合理的函数,你可以将它们都内联到一个大型函数中,再从中提炼出合理的小型函数。
    如果你在inline method的时候碰上了递归调用,多返回点,内联至另一个对象而该对象并无提供访问函数时,说明不应该使用这个重构方法。
  3. inline temp
    在这里插入图片描述在这里插入图片描述
    多半是作为replace temp with query的一部分使用的,如果这个临时变量妨碍了其他的重构手法,比如extract method,就应该将它内联化。
  4. replace temp with query
    在这里插入图片描述
    在这里插入图片描述
    临时变量的问题在于:它们是暂时的,而且只能在函数内使用。所以它们会让你写出更长的函数,因为这样你猜呢访问到临时变量。如果把临时变量替换为一个查询,那么一个类的所有函数杜能获得这份信息,能使你为这个类编写更清晰的代码。
    我们常常使用临时变量保存循环中的累加信息。在这种情况下,整个循环都可以被提炼为一个独立函数,有时一个循环中会累加好几个值,那么就多创建几个函数,把所有临时变量都替换为查询。
  5. introduce explaining variable
    在这里插入图片描述
    在这里插入图片描述
    在条件逻辑中,可以将每个条件子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的意义。在较长算法中,也可以用临时变量来解释每一步运算的意义。
    用extract method一般也能得到一样的效果,但是,如果在extract method要花费更大工作量时,比如要处理一个拥有大量局部变量的算法,这种情况下就用本策略,然后再考虑下一步该怎么办。
  6. Split Temporary Variable 分解临时变量
    在这里插入图片描述
    在这里插入图片描述
    有很多临时变量用于保存一段代码的运算结果。如果它们被赋值超过一次,说明承担了一个以上的责任,应该被替换为多个临时变量。
  7. remove assignments to parameters
    在java中,不要对参数赋值,那会混淆了值传递和引用传递。(个人感觉java程序员必须要弄清楚值传递和引用传递的区别,这算最最基本的基本功了)
  8. replace method with method object
    有的函数因为包含很多局部变量,想要extract method非常困难,那么可以考虑新建一个类,把所有局部变量变成新类的field,然后把函数的代码复制过来,源函数改为调用新类的同名方法,这个新类的实例就称为method object,新类里面就可以做extract method了。
  9. substitute algorithm
    有时你会发现在原先的做法之外,还有更简单的解决方案,此时你就可以考虑替换掉原来的算法。不过在进行该重构前,确认你对原算法非常了解。如果测试结果不同于原先,以旧算法为比较参照标准。
第1章 重构,第个案例1 1.1 起点1 1.2 重构的第步7 1.3 分解并重组statement()8 1.4 运用多态取代与价格相关的条件逻辑34 1.5 结语52 第2章 重构原则53 2.1 何谓重构53 2.2 为何重构55 2.3 何时重构57 2.4 怎么对经理说60 2.5 重构的难题62 2.6 重构设计66 2.7 重构与性能69 2.8 重构起源何处71 第3章 代码的坏味道75 3.1 DuplicatedCode(重复代码)76 3.2 LongMethod(过长函数)76 3.3 LargeClass(过大的类)78 3.4 LongParameterList(过长参数列)78 3.5 DivergentChange(发散式变化)79 3.6 ShotgunSurgery(霰弹式修改)80 3.7 FeatureEnvy(依恋情结)80 3.8 DataClumps(数据泥团)81 3.9 PrimitiveObsession(基本类型偏执)81 3.10 SwitchStatements(switch惊悚现身)82 3.11 ParallelInheritanceHierarchies(平行继承体系)83 3.12 LazyClass(冗赘类)83 3.13 SpeculativeGenerality(夸夸其谈未来性)83 3.14 TemporaryField(令人迷惑的暂时字段)84 3.15 MessageChains(过度耦合的消息链)84 3.16 MiddleMan(中间人)85 3.17 InappropriateIntimacy(狎昵关系)85 3.18 AlternativeClasseswithDifferentInterfaces(异曲同工的类)85 3.19 IncompleteLibraryClass(不完美的库类)86 3.20 DataClass(纯稚的数据类)86 3.21 RefusedBequest(被拒绝的遗赠)87 3.22 Comments(过多的注释)87 第4章 构筑测试体系89 4.1 自测试代码的价值89 4.2 JUnit测试框架91 4.3 添加更多测试97 第5章 重构列表103 5.1 重构的记录格式103 5.2 寻找引用点105 5.3 这些重构手法有多成熟106 第6章 重新组织函数109 6.1 ExtractMethod(提炼函数)110 6.2 InlineMethod(内联函数)117 6.3 InlineTemp(内联临时变量)119 6.4 ReplaceTempwithQuery(以查询取代临时变量)120 6.5 IntroduceExplainingVariable(引入解释性变量)124 6.6 SplitTemporaryVariable(分解临时变量)128 6.7 RemoveAssignmentstoParameters(移除对参数的赋值)131 6.8 ReplaceMethodwithMethodObject(以函数对象取代函数)135 6.9 SubstituteAlgorithm(替换算法)139 第7章 在对象之间搬移特性141 7.1 MoveMethod(搬移函数)142 7.2 MoveField(搬移字段)146 7.3 ExtractClass(提炼类)149 7.4 InlineClass(将类内联化)154 7.5 HideDelegate(隐藏“委托关系”)157 7.6 RemoveMiddleMan(移除中间人)160 7.7 IntroduceForeignMethod(引入外加函数)162 7.8 IntroduceLocalExtension(引入本地扩展)164 第8章 重新组织数据169 8.1 SelfEncapsulateField(自封装字段)171 8.2 ReplaceDataValuewithObject(以对象取代数据值)175 8.3 ChangeValuetoReference(将值对象改为引用对象)179 8.4 ChangeReferencetoValue(将引用对象改为值对象)183 8.5 ReplaceArraywithObject(以对象取代数组)186 8.6 DuplicateObservedData(复制“被监视数据”)189 8.7 ChangeUnidirectionalAssociationtoBidirectional(将单向关联改为双向关联)197 8.8 ChangeBidirectionalAssociationtoUnidirectional(将双向关联改为单向关联)200 8.9 ReplaceMagicNumberwithSymbolicConstant(以字面常量取代魔法数)204 8.10 EncapsulateField(封装字段)206 8.11 EncapsulateCollection(封装集合)208 8.12 ReplaceRecordwithDataClass(以数据类取代记录)217 8.13 ReplaceTypeCodewithClass(以类取代类型码)218 8.14 ReplaceTypeCodewithSubclasses(以子类取代类型码)223 8.15 ReplaceTypeCodewithState/Strategy(以State/Strategy取代类型码)227 8.16 ReplaceSubclasswithFields(以字段取代子类)232 第9章 简化条件表达式237 9.1 DecomposeConditional(分解条件表达式)238 9.2 ConsolidateConditionalExpression(合并条件表达式)240 9.3 ConsolidateDuplicateConditionalFragments(合并重复的条件片段)243 9.4 RemoveControlFlag(移除控制标记)245 9.5 ReplaceNestedConditionalwithGuardClauses(以卫语句取代嵌套条件表达式)250 9.6 ReplaceConditionalwithPolymorphism(以多态取代条件表达式)255 9.7 IntroduceNullObject(引入Null对象)260 9.8 IntroduceAssertion(引入断言)267 第10章 简化函数调用271 10.1 RenameMethod(函数改名)273 10.2 AddParameter(添加参数)275 10.3 RemoveParameter(移除参数)277 10.4 SeparateQueryfromModifier(将查询函数和修改函数分离)279 10.5 ParameterizeMethod(令函数携带参数)283 10.6 ReplaceParameterwithExplicitMethods(以明确函数取代参数)285 10.7 PreserveWholeObject(保持对象完整)288 10.8 ReplaceParameterwithMethods(以函数取代参数)292 10.9 IntroduceParameterObject(引入参数对象)295 10.10 RemoveSettingMethod(移除设值函数)300 10.11 HideMethod(隐藏函数)303 10.12 ReplaceConstructorwithFactoryMethod(以工厂函数取代构造函数)304 10.13 EncapsulateDowncast(封装向下转型)308 10.14 ReplaceErrorCodewithException(以异常取代错误码)310 10.15 ReplaceExceptionwithTest(以测试取代异常)315 第11章 处理概括关系319 11.1 PullUpField(字段上移)320 11.2 PullUpMethod(函数上移)322 11.3 PullUpConstructorBody(构造函数本体上移)325 11.4 PushDownMethod(函数下移)328 11.5 PushDownField(字段下移)329 11.6 ExtractSubclass(提炼子类)330 …… 第12章 大型重构359 第13章 重构,复用与现实379 第14章 重构工具401 第15章 总结409 参考书目413 要点列表417 索引419
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值