《重构·改善既有代码的设计》阅读笔记一、重构的原理1、重构的定义重构:对软件内部结构的一种调整。目的是在不改变软件可观察行为的情况下,提高其可理解性,降低其修改成本。2.为什么要重构在开始讲为什么要重构之前,先说说为什么很多程序员不喜欢重构。时间紧迫,一直忙于实现功能。感觉重构影响效率,重构不是性能。运行中需要修改程序,可能会引入一些不易察觉的错误。对重构没有系统的、全局的理解。面对一堆烂代码,又没有重构技巧的指导,只能想着哪里改,哪里接手别人。在写代码的时候,代码已经败坏到无可救药的地步,重构的成本极高。是的,对于当时没有重构就完成了功能的程序员来说是比较幸福的,但是有没有想过以后接手这段代码呢?人们感觉如何?当我遇到糟糕的代码时,我经常抱怨以前的开发人员,但我是否有持续重构的习惯?以后是不是也想被别人吐槽?是现在花几分钟重构成本高,还是以后花半天时间找bug成本高?以后有新需求来的时候,不重构就改,成本高吗?还是重构后的变更成本高?所以希望大家多多学习重构,做一个有教养的工程师,这样大家都好。以下是重构的几个原因:重构改进软件设计重构使软件更易于理解重构有助于发现错误重构提高编程速度项目在发展,代码不断堆积。如果没有人对代码的质量负责,代码永远会朝着越来越乱的方向发展。当混乱达到一定程度时,量变导致质变,项目的维护成本已经高于重新开发一套新代码的成本。如果你想重构,没有人能做到。每个软件模块具有三个职责。第一个责任是它在运行时做了什么。第二个责任是它响应变化,一个很难变化的模块是有问题的,即使能用也需要修改。第三个职责是与读者交流。——《敏捷开发·纪念版》3、什么时候在增加功能的时候重构,重构只有三件事,三个重构(三法则)修复错误的时候重构,review代码的时候重构删除、修改、检查”,目的是扩展和修改我们的系统是及时的,事半功倍的。注意重构是连续的,不要老想着“憋大招”,等代码坏到一定程度再重构。有时候项目代码太多,而且重构很难做到彻底,最后搞出一个“四怪”,这就更麻烦了!所以希望代码烂到一定程度后,集中重构来解决所有问题是不现实的.我们必须探索一条可持续发展的方式。4.重构的难点重构数据库修改接口(尤其是发布的接口)难以通过重构完成的设计变更什么时候不应该重构重构的代码太混乱,不如重写一个简单的项目来了deadline临近5.重构与设计重构与设计是相辅相成的。重构可以在不损失灵活性的情况下使设计更简单,这也降低了设计过程的难度,减轻了设计的压力。6.重构与性能除了对性能要求严格的实时系统,在任何其他情况下“写快软件”的秘诀是:先写可调整的软件,然后调整它以获得足够的速度。除非某段代码是影响系统性能瓶颈的代码,否则不要为了性能优化过度牺牲代码质量。这种优化的投入产出比不高,增加了代码实现的难度,牺牲了代码的可读性。性能提升不明显。二、Badsmellofcode1.DuplicatedCode(重复代码)第一个不好的气味是DuplicatedCode。如果在不止一个地方看到相同的重复结构,那么就可以确定这种难闻的气味,尝试将它们组合起来对于同一个类中包含相同表达式的两个或多个函数,使用ExtractMethod(提取方法)来提取重复的代码,然后将新提取的函数引用为包含相同表达式的同级子类。Method)将重复的代码细化,然后PullUpMethod(将函数上移)到互为兄弟的超类的子类中,包含一些相同的表达式。通常,两个相似的函数以相同的顺序执行大致相同的操作,但每个操作并不完全相同。使用ExtractMethod(提取方法)提取重复代码。您可能会发现可以使用FormTemplateMethod(整形模板功能)。有些函数用不同的算法做同样的事情。使用SubstituteAlgorithm(替换算法)替换其他函数。重复代码出现在两个不相关的类中。使用ExtractClass(提取类)将重复的代码提取到一个独立的类中,然后引用新的类。2、LongMethod(超长函数)程序员早就意识到,程序越长,越难理解。在早期的编程语言中,调用子程序需要额外的开销,因此不愿意使用小函数。现在OO语言几乎已经完全免除了调用过程中动作的开销,即使函数名比实现长,关键是说明用途(而不是实现方法)。没有局部变量。使用ExtractMethod(提取函数)将函数提取出来,使其具有局部变量。于是用ReplaceTempwithQuery(用query替换临时变量)让结构清晰,再用ExtractMethod(抽取函数)。如果提取函数后发现新的函数给参数赋值,应用RemoveAssignmentstoParameters参数赋值)3.LargeClass(太大的类)如果想用单个类做太多的事情,有其中的实例变量通常太多。一旦出现这种情况,就会出现重复代码。当发现在一个类中,并不是所有的实例变量在任何时候都被使用,或者一个类中出现多个具有相同前缀或结尾的变量时,构造的动机就出现了。如果一个类被用于很多类似的字段,而方法都是只与某些字段相关,那么可以考虑这些字段是否应该属于另一个类ExtractClass(提取类)如果发现类中的某些行为只被某些实例使用,而其他的不使用,你可以试试ExtractSubClass或ExtractClass,这两者之间的选择是委托和继承之间的选择,ExtractSubClass通常更容易,但它有局限性:一旦对象被创建,您无法再更改对象行为。委托更灵活(策略模式)。4.LongParamenterList(参数列表太长)太长的参数列表很难看懂,参数太多会造成不一致,不好用,一旦需要的数据多了就得修改。如果将对象传递给函数,则大部分修改都没有必要,因为您可能只需要在函数中添加一两个请求即可获取更多数据。如果您向现有对象发出请求,您可以替换参数,那么您应该激活方法ReplaceParameterwithMethod(用函数替换参数)。这里的“已有对象”可能是函数所属类中的一个字段,也可能是另一个参数。您还可以使用PreserveWholeObject从同一对象收集一堆数据并将它们替换为该对象。如果某些数据缺少合理的对象属性,可以使用IntroduceParameterObject为其创建参数对象。这里有一个重要的例外:有时你显然不想在“被调用对象”和“更大的对象”之间造成某种差异。一种依赖。这时候从对象中拆解参数语句,单独作为参数使用也是合理的。但是要注意它引发的代码。如果参数列表太长或变化太频繁,你需要重新考虑你的依赖结构。5.DivergentChange(发散变化)如果一个类由于不同的原因经常向不同的方向变化,就会出现发散变化。使用ExtractClass(提取类)将更改的责任提取到一个新类中。你看A班说,“嗯,如果新增一个数据库,我就得修改这三个功能;如果出现一个新的工具,我就得修改这四个功能”。此时,将这个类拆分成两个可能会更好,这样每个类只需要修改一个变化。当然,您通常只有在添加新数据库或新金融工具时才会发现这一点。所有对某个外部变化的响应修改都应该只发生在一个单独的类中,这个新类中的所有内容都应该实现这个变化。要做到这一点,您应该找到所有由特定原因引起的变化,然后使用ExtractClass(提取类)将它们提取到另一个类中。6.ShotgunSurgery(shotgunmodification)如果每次遇到某个改动都要在很多不同的职业中做小修改,那你面对的臭味就是ShotgunSurgery。如果你需要在很多地方修改代码,你不仅很难找到它们,而且很容易忘记一个重要的修改。在这种情况下你应该使用MoveMethod(移动函数)和MoveField(移动字段)将所有需要修改的代码放到同一个类中。如果目前没有合适的类,请创建一个。如果你将你的代码移动到同一个类,而原始类几乎是空的,请尝试通过内联类摆脱这些现在冗余的类。发散变化(DivergentChange)指的是“一个类受到多个变化的影响”,而ShotgunSurgery指的是“引入一个变化触发多个类的修改”。在这两种情况下,您都希望组织代码,使“外部更改”和“需要修改的类”倾向于一一对应。7.FeatureEnvy(依恋情结)功能对某个班级的兴趣大于对自己班级的兴趣。将此功能移动到另一个位置,它应该去的地方。MoveMethod(移动函数)如果一个函数使用了几个类的函数,就要判断哪个类的这个函数使用的数据最多,然后把这个函数和那些数据放在一起。ExtractMethod(提取函数)、MoveMethod(移动函数)8、DataClumps(数据泥球)数据项就像孩子一样,总是喜欢成群结队地呆在一起,如果删除了众多数据中的一个,其他数据就失去了意义,应该为它们生成一个新对象。如果一个类中有很多相关的数据Field,那么就要考虑为这些相关的数据建立一个新的归宿。ExtractClass(提取类)创建一个新的数据对象如果函数参数引用了很多相关的Field,那么就要考虑把这些分散的参数做成参数对象。IntroduceParameterObject(引用参数对象)如果一个函数的参数来自同一个对象的几个属性,可以考虑引用对象。因为如果被调用的函数改变了参数,你必须找到并修改这个函数的所有调用PreserveWholeObject(保持对象完好无损)第二点和第三点类似,只是第二点需要创建一个新的类来声明字段,第一个9.PrimitiveObsession(基本类型偏执)一句话,我只喜欢在原来代码的基础上增加基本类型字段,如果不喜欢提取对象,可以用ReplaceDataValuewithObject(用对象替换数据值)将单独存在的数据值替换为对象。如果要替换的数据值是类型代码,且不影响行为,可以使用ReplaceTypeCodewithClass(用类替换类型代码)来替换。如果您有与类型代码相关的条件表达式,则可以使用ReplaceTypeCodewithSubClasses(将类型代码替换为子类)或ReplaceTypeCodewithState/Strategy(将类型代码替换为State/Strategy)。如果你有一组总是放在一起的字段,使用ExtractClass如果你在参数列中看到原始数据类型,尝试IntroduceParameterObject来选择数据,你可以使用ReplaceArraywithObject(用对象替换数组)10.Switch语句(Switch惊悚出现)你经常会发现switch语句散落在不同的地方。如果你想为它添加一个新的case子句,你必须找到所有的switch语句并修改它们。面向对象中的多态性概念可以为此带来优雅的解决方案。根据类型选择不同行为的条件表达式。ReplaceConditionalwithPolymorphism(用多态替换条件表达式)有一个影响行为的类型代码,但是你不能通过继承来消除它,或者类型代码的值在对象的生命周期中发生变化ReplaceTypeCodewithState/Strategy(用状态/策略替换类型代码)如果单个函数有一些选择案例,并且您不想更改它们,那么多态性就有点矫枉过正了。ReplaceParameterwithExplicitMethods(用显式函数替换参数)11.ParallelInheritanceHierarchies(并行继承系统)并行继承系统实际上是shotgun修改的特例。在这种情况下,无论何时为一个类添加子类,都必须为另一个类添加相应的子类。如果你发现一个继承系统的类名前缀和另一个继承系统的类名前缀完全一样,你就闻到了这种难闻的气味。让一个继承系统的实例引用另一个继承系统的实例。如果使用MoveMethod(移动函数)和MoveField(移动字段)可以消除在引用端不可见的继承系统。12.LazyClass(冗余类)如果一个类的收入不值得,它就应该消失。项目中经常会出现某个类本来值自己的值,重构后让它体积缩小,不再做那么多工作;或者开发人员提前计划了一些变化,并添加了一个类来应对这些变化,但这些变化并没有真正发生。如果有些子类做的还不够,就用CollapseHierarchy(折叠继承系统)对于几乎没用的组件,用InlineClass(内联类)13、SpeculativeGenerality(以后讲)如果不需要就删掉。如果您的某个抽象类不是很有用,请使用CollapseHierarchy(折叠继承系统)。可以使用内联类(内联类)删除不必要的委托。如果有些参数不用,可以在上面实现RemoveParameter(移除参数)。如果函数名有多余的抽象意义,就应该在其上实现RenameMethod(函数重命名)。14.TemporaryField(易混淆的临时字段)如果类中有一个复杂的算法,需要多个变量,可能经常会导致出现难闻易混淆的临时字段(TemporaryField)。由于实施者不想传递一长串参数,他将它们全部放入字段中。但是这些字段只有在使用算法时才有效,否则只会让人感到困惑。这时候就可以使用ExtractClass将这些变量和它们相关的函数提取到一个独立的类中。15.MessageChains(过耦合消息链)如果你看到用户从一个对象请求另一个对象,然后从后者请求另一个对象,然后类似地请求另一个对象:getPerson().getDepartment()。getAddress().getStreet()表示消息链使用HideDelegate(隐藏委托关系)。理论上,消息链中的任何对象都可以被重构,但这样做会使一系列对象变成中间人(MiddleMan)。先观察消息链最后拿到对象干什么,看能不能用ExtractMethod(提取函数)把使用对象的代码提取成一个独立的函数,然后用MoveMethod(移动函数)推送这个函数进入消息链16、中间人(middleman)对象的基本特征之一是封装——对外界隐藏其内部细节。封装通常伴随着委托。但是人们可能会过度使用委派。你可能会看到一个类接口的一半功能被委托给其他类,这是过度使用。使用RemoveMiddleMan(去除中间人)直接对付真正的责任对象。如果只有几个不做实际事情的函数,就用InlineMethod(内联函数)把它们放到调用者里面。如果这些中间人(MiddleMan)等行为,你可以使用ReplaceDelegationwithInheritance(继承而不是委托)将其变成负责对象的子类,这样你就可以扩展愿意对象的行为而不用担心so许多委托行动。17.InappropriateIntimacy(亲密关系)有时你会看到两个班级过于亲密,花太多时间探索彼此的私密部分。你可以使用MoveMethod(移动功能)和MoveField(移动领域)来帮助他们划清界限,从而减少亲密感。您还可以使用ChangeBidirectionalAssociationtoUnidirectional(将双向关联更改为单向关联)让一个类影响另一个类。剪断爱线如果两个类真的一致,可以使用ExtractClass(提取类)将两者的共同点提取到安全的地方,让他们公开使用新类或者使用HideDelegate(隐藏委托关系)让另一个类替他们做传相思18.AlternativeClasseswithDifferentInterfaces(目的相同的类)两个函数做同样的事情但是签名不同使用RenameMethod(函数重命名)重命名函数,然后使用Move方法(移动函数)反复移动一些Behavior被移动到类中,直到两者的协议一致。如果你不得不重复和冗余地移动代码来完成这些,你可以使用ExtractSuperclass(提取超类)来赎回自己。19.IncompleteLibraryClass(不完善的库类)如果要修改库类的一两个功能,可以使用IntroduceForeignMethod(引入额外的功能)如果要添加很多额外的行为,使用IntroduceLocalExtension(引入localextension),好处是“函数和数据统一封装,让其他类太复杂”20、DataClass(Naivedataclasses)Naivedataclasses是:它们有一些字段,以及用于访问(读写)的函数这些领域,没有别的。这样的类只是愚蠢的数据容器,它们几乎肯定会被其他类以不适当的技巧操纵。使用EncapsulateField(封装字段)来封装这些字段。如果这些类有使用容器类的字段,使用EncapsulateCollection(封装集合)。对于那些不应该被其他类修改的字段,请使用RemoveSettingMethod(去除设置值函数),找到这些getter/setter函数的调用点。尝试使用Move方法将这些调用行为移动到数据类。如果整个函数不能移动,可以使用ExtractMethod(提取函数)移动。紧接着,就可以用HideMethod(隐藏函数)把这些取值/置值函数隐藏起来,然后用PushDownMethod(函数下移)和PushDownField(字段下移)把不用的函数全部下推给那位大哥.这样,超类只持有所有子类共享的东西。使用ReplaceInheritancewithDelegation(用委托代替继承)重构22.Comments(过多的注释)并不意味着注释不应该写,因为人们经常把注释当作“除臭剂”。通常情况下,您看到一段带有长注释的代码,然后意识到这些注释仍然存在,因为代码很糟糕。如果您需要注释来解释一段代码的作用,请尝试ExtractMethod(提取函数)。如果函数已被提取,但您仍然需要注释来解释其行为,请尝试重命名方法(函数重命名)。如果你需要注释来解释一些系统需求规范,试试IntroduceAssertion(引入断言)如果你不知道该怎么做,这是引入注释的好时机
