头图

《重构·改善既有代码的设计》读书笔记

一、重构原则

1、重构定义

重构:对软件内部结构的一种调整。目的是在不改变软件可观察行为的前提下,提高其可理解性降低其修改成本

2、为何重构

在开始说为何重构之前,先说一下很多程序员为何不喜欢重构。

  • 时间紧,一直忙着实现功能,觉得重构影响效率,而且重构不算绩效,简直吃力不讨好
  • 觉得代码写完之后可能以后都不是自己维护了,只顾当时快速实现功能
  • 重构有风险,它必须修改运作中的程序,这可能引入一些不易察觉的错误
  • 对重构没有系统性、全局性的认识,面对一堆烂代码,没有重构技巧的指导,只能想到哪改到哪
  • 接手别人写的代码时,代码已经腐化到无可救药了,重构成本极高

是的,对当时做完功能的程序员来说不重构确实相对痛快,但是有没有想过将来接手这块代码的人的感受?自己遇到烂代码时都经常吐槽之前的开发者,但自己有养成持续重构的习惯吗?难道你也想在将来被别人吐槽?是现在花几分钟重构的成本高,还是将来找一个bug花半天的时间成本高?当以后新来一个需求时,是不重构的改动成本高?还是重构之后的改动成本高?所以希望大家多平常了解一下重构相关知识,做一个有素养的工程师,这样大家都好。下面罗列了几个重构的原因:

  • 重构改进软件设计
  • 重构使软件更容易理解
  • 重构帮助找到bug
  • 重构提高编程速度

项目在演进,代码不停地在堆砌。如果没有人为代码的质量负责任,代码总是会往越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。

每个软件模块都有三项职责。第一项职责是它运行时所完成的功能。第二项职责是它要应对变化,一个难以改变的模块是有问题的,即便能够工作,也需要对它进行修改。第三项职责是要和读者进行沟通。 ——《敏捷开发·纪念版》

3、何时重构

  • 添加功能时重构
  • 事不过三,三则重构(三次法则)
  • 修补错误时重构
  • 复审代码时重构

以上四点被称为重构时机的“增删改查”,目的是想及时通过最小的努力就能对我们的系统进行扩展和修改。注意,重构是持续的,不要总想着“憋大招”,等代码烂到一定程度再重构,有时候项目代码太多了,重构很难做得彻底,最后又搞出来一个“四不像的怪物”,这就更麻烦了!所以,寄希望于在代码烂到一定程度之后,集中重构解决所有问题是不现实的,我们必须探索一条可持续、可演进的方式。

4、重构的难题

  • 重构数据库
  • 修改接口(特别是已发布的接口)
  • 难以通过重构手法完成的设计改动
  • 何时不该重构

    • 重构的代码太混乱,还不如重写一个来的简单
    • 项目已经接近最后期限

5、重构与设计

重构与设计是互补的,重构可以带来更简单的设计,同时又不损失灵活性,这也降低了设计过程的难度,减轻了设计的压力。

6、重构与性能

除了对性能有严格要求的实时系统,其他任何情况下“编写快速软件”的秘密就是:首先写出可调的软件,然后调整它以求获取足够的速度。

除非某段代码是影响系统性能瓶颈的代码,否则不要过度地为了性能优化而牺牲代码质量,这样优化的投入产出比并不高,增加了代码实现的难度、牺牲了代码的可读性,性能上的提升却并不明显。

二、代码的坏味道

1、Duplicated Code(重复代码)

坏味道首当其冲的就是Duplicated Code,如果你在一个以上的地点看到相同的重复结构,那么这个坏味道就可以确定了,设法将它们合而为一

  • 同一个类中两个或更多的函数含有相同的表达式

    利用Extract Method(提炼方法)提炼重复代码,然后引用新提炼的函数

  • 互为兄弟的子类含有相同的表达式

    利用Extract Method(提炼方法)提炼重复代码,然后Pull Up Method(函数上移)堆到超类

  • 互为兄弟的子类,含有部分相同的表达式

    一般常见相似的两个函数以相同的顺序执行大致的操作,但是各操作不完全一样

    利用Extract Method(提炼方法)提炼重复代码,可能发现是可以运用Form Template Method(塑造模板函数)

  • 有些函数以不用的算法做相同的事

    使用Substitute Algorithm(替换算法)将其他函数替换掉

  • 两个互不相关的类出现Duplicated Code

    利用Extract Class(提炼类) 将重复代码提炼到一个独立的类中,然后引用新类。

2、Long Method(过长的函数)

很久以前程序员就已经认识到程序越长越难理解,在早期编程语言,调用子程序需要额外开销,所以不愿意使用小函数。现在OO语言几乎已经完全免除了进程内的调用动作开销

函数命名原则:每当感觉需要注释说明点什么的时候,就可以把需要说明的东西写进一个独立函数中,并以其"用途"命名,哪怕函数名比实现还要长,关键是要说明用途(而非实现手法)

  • 无局部变量

    利用Extract Method(提炼函数)提炼函数即可

  • 有局部变量

    如果发现局部变量是保存某一个表达式的运算结果,那么用Replace Temp with Query(以查询取代临时变量)使结构清晰后,再Extract Method(提炼函数),如果提炼了函数后发现新函数对参数赋值了,应用Remove Assignments to Parameters(移除对参数的赋值)

3、Large Class(过大的类)

如果想用单个类做太多事情,其内往往就出现了太多实例变量,一旦如此,duplicate code也就要出现了。当发现一个类中,并非所有时刻都使用所有实例变量,或者某个类中出现多个变量有着相同前缀或结尾,构的动机就出现了

  • 如果一个类中用于很多相似的字段,而且方法又都只是和某几个字段有关系,那么可以考虑这些字段是不是应该属于另一个类Extract Class(提炼类)
  • 如果你发现类中的某些行为只被一部分实例用到,其它没有用到,可以尝试Extract SubClass(提炼子类)或者Extract Class(提炼类),这两种的抉择就是委托和继承之间的抉择,Extract SubClass(提炼子类)通常更容易,但它也有限制:一旦对象创建完成,你无法再改变对象行为。而委托更灵活一些(策略模式)。

4、Long Paramenter List(过长参数列)

太长的参数列难以理解,太多的参数会造成前后不一致,不易使用,而且一旦你需要更多的数据,就不得不修改它。如果将对象传递给函数,大多数修改都将没有必要,因为你很可能只需要在函数内增加一两条请求,就能得到更多数据

  • 如果向已有的对象发出一条请求,就可以取代一个参数,那么你应该激活手法 Replace Parameter with Method(以函数取代参数)。在这里"已有对象"可能是函数所属类内的一个字段也可能是另一个参数。
  • 你也可以运用 Preserve Whole Object(保持对象完整) 将来自同一个对象的一堆数据收集起来,并以该对象替换它们。如果某些数据缺乏合理的对象归属,可使用Introduce Parameter Object 为它们制造出一个参数对象
  • 这里有一个重要的例外:有时候你明显不希望造成"被调用对象"与"较大对象"间的某种依赖关系。这时候将参数句从对象中拆解出来单独作为参数,也很合情合理。但是请注意其所引发的代码。如果参数列太长或变化太频繁,你就需要重新考虑自己的依赖结构了。

5、Divergent Change(发散式变化)

如果某个类经常因为不同的原因在不同方向上发生变化,发散式变化就出现了

  • 使用Extract Class(提炼类)把变化的职责提炼到新的类

你看着一个类说:"呃,如果新加入一个数据库,我必须修改这三个函数;如果新出现一种工具,我必须修改这四个函数。"那么此时也许将这个类分成两个会更好,这么一来每个类就可以只因为一种变化而需要修改。当然,往往只有在加入新数据库或者新金融工具后,你才能发现这一点。针对某一个外界变化的所有响应修改,都只应该发生在单一类中,而这个新类内的所有内容都应该发硬此变化。为此,你应该找出某特定原因而造成的所有变化,然后运用Extract Class(提炼类) 将他们提炼到另一个类中。

6、Shotgun Surgery(霰弹式修改)

如果每遇到某种变化,你都必须在许多不同的类作出小修改,你所面临的坏味道就是Shotgun Surgery,你如果需要修改代码有很多处,你不但很难找到它们,也很容易忘记某个重要的修改。

  • 这种情况下你应该使用 Move Method (搬移函数)Move Field (搬移字段)把所有需要修改的代码放进同一个类。如果眼下没有合适的类,就创造一个。如果将代码移动到同一个类,使原始类几乎为空,请尝试通过Inline Class摆脱这些现在多余的类。

发散式变化(Divergent Change) 是指"一个类受多种变化的影响",霰弹式修改(Shotgun Surgery)是指"一种变化引入引发多个类的修改"。这两种情况下,你都会希望整理代码,使"外界变化"与"需要修改的类"趋于一一对应

7、Feature Envy(依恋情结)

函数对于某个类的兴趣高过对自己所处类的兴趣。

  • 把这个函数移至另一个地点,移到它该去的地方。Move Method(搬移函数)
  • 如果一个函数用到几个类的功能,则该判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据摆在一起。Extract Method(提炼函数)Move Method(搬移函数)

8、Data Clumps(数据泥团)

数据项就像小孩子,总喜欢成群结对地的待在一块,如果删掉众多数据中的一项,其他数据因而失去意义,就应该为它们产生一个新对象

  • 如果一个类里有很多有关系的数据Field,那么就要考虑为这些有关系的数据建立一个新家。Extract Class(提炼类) 创建数据新对象
  • 如果函数参数引用很多有关系的Field,那么就要考虑让这些分散的参数变成参数对象。Introduce Parameter Object (引用参数对象)
  • 如果一个函数的参数来自同一个对象的若干个属性,可以考虑引用对象。因为如果被调用的的函数改变参数,必须查找并修改这个函数的所有调用Preserve Whole Object(保持对象完整)

    第二点与第三点类似,只不过第二点需要新建一个类来声明字段,第三点不用

9、Primitive Obsession(基本类型偏执)

简单一句话就是只喜欢在原代码的基础上加基本类型字段,不喜欢提取对象

  • 可以运用Replace Data Value with Object (以对象取代数据值)将原本单独存在的数据值替换为对象。
  • 如果想要替换的数据值是类型码,而它不影响行为,则可以运用Replace Type Code with Class(以类取代类型码)将它换掉。
  • 如果你有与类型码相关的条件表达式可以运用Replace Type Code with SubClasses(以子类取代类型码) 或者 Replace Type Code with State/Strategy (以State/Strategy取代类型码 )加以处理。
  • 如果你有一组总是被放在一起的字段,可以运用Extract Class(提炼类)
  • 如果在参数列中看到基本数据类型,不妨试试Introduce Parameter Object (引用参数对象)
  • 如果你发现自己正从数组中挑选数据,可以运用 Replace Array with Object(以对象取代数组)

10、Switch Statements(Switch惊悚现身)

你常会发现switch语句散布于不同地点。如果要为它添加一个新的case子句,就必须找到所有switch语句并修改它们。面向对象中的多态概念可为此带来优雅的解决办法。

  • 条件表达式,它根据类型的不同而选择不同的行为。Replace Conditional with Polymorphism (以多态取代条件表达式)
  • 有个类型码,它会影响行为,但你无法通过继承的方式消除它,或者类型码的数值在对象的生命周期中发生变化Replace Type Code with State/Strategy (以状态/策略取代类型码)
  • 如果单一函数 有些选择事例,且不想改动它们,那么多态就有点杀鸡用牛刀了。Replace Parameter with Explicit Methods (以明确函数取代参数)

11、Parallel Inheritance Hierarchies(平行继承体系)

平行继承体系其实是散弹式修改的特殊情况。这种情况下,每当你为某个类增加一个子类,必须也为另一个类相应增加一个子类。如果你发现某个继承体系的类名称前缀和另一个继承体系的类名称前缀完全相同,便是闻到了这种坏味道

  • 让一个继承体系的实例引用另一个继承体系的实例,,如果在运用Move Method (搬移函数)Move Field(搬移字段)就可以将引用端的继承体系消弭于无形

12、Lazy Class(冗赘类)

如果一个类的所得并不值其身价,他就应该消失。项目中经常会出现这样的情况:某个类原本对得起自己的价值,但重构使它身形缩水,不再做那么多工作;或开发者事先规划了某些变化,并添加一个类来应付这些变化,但变化实际没有发生。

  • 如果某些子类没有做足够的工作,使用Collapse Hierarchy(折叠继承体系)
  • 对于几乎没用的组件,使用Inline Class(将类内联化)

13、Speculative Generality(夸夸其谈未来性)

如果这段代码当前用不到,就删掉它

  • 如果你的某个抽象类没有太大作用,请用Collapse Hierarchy(折叠继承体系)
  • 不必要的委托可以用Inline Class(将类内联化)除掉
  • 如果函数的某些参数未被用上,可对它实施Remove Parameter(移除参数)
  • 如果函数名带有多余的抽象意味,应该对它实施Rename Method(函数改名)

14、Temporary Field(令人迷惑的暂时字段)

如果类中有一个复杂算法,需要好几个变量,往往就可能导致坏味道令人迷惑的临时字段(Temporary Field)出现。由于实现者不希望传递一长串参数,所以他把这些参数都放进字段。但是这些字段只在使用该算法时才有效,其他情况下只会让人迷惑。

  • 这时可以利用 Extract Class (提炼类)把这些变量和其相关函数提炼到一个独立的类中。

15、Message Chains(过度耦合的消息链)

如果你看到用户向一个对象请求另一个对象,然后在向后者请求另一个对象,然后在请求另一个对象类似: getPerson().getDepartment().getAddress().getStreet() 就是消息链

  • 使用Hide Delegate(隐藏委托关系)。理论上讲可以重构消息链上的任何一个对象,但这么做会把一系列对象都变成中间人(Middle Man)。
  • 先观察消息链最终得到对象来干什么,看能否以Extract Method(提炼函数)把使用该对象的代码提炼到一个独立的函数中,再用Move Method(搬移函数)把这个函数推入消息链

16、Middle Man(中间人)

对象的基本特征之一就是封装——对外部世界隐藏其内部细节.封装往往伴随着委托(delegate)。但人们可能过度运用委托。你也许会看到某个类接口有一半的函数都委托给其它类,这样就是过度运用。

  • 使用Remove Middle Man(移除中间人)直接和真正的负责的对象打交道
  • 如果这样不干实事的函数只有少数几个运用,使用Inline Method(内联函数) 把他们放进调用端
  • 如果这些中间人(Middle Man)还有其它行为,可以运用Replace Delegation with Inheritance(以继承取代委托)把它变成实责对象的子类,这样既可以扩展愿对象行为,又不必担心那么多委托动作。

17、Inappropriate Intimacy(狎昵关系)

有时候你会看到两个类过于亲密,花费太多时间去探究彼此的private成分。

  • 可以采用Move Method(搬移函数)Move Field(搬移字段)帮它们划清界限,从而减少狎昵行径
  • 也可以运用Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)让其中一个类对另一个斩断情丝
  • 如果两个类实在情投意合,可以使用Extract Class(提炼类)把两者共同点提炼到一个安全地点,让它们坦荡使用新类
  • 或者使用Hide Delegate(隐藏委托关系)让另一个类为它们传递相思情

18、Alternative Classes with Different Interfaces(异曲同工的类)

两个函数做同一件事却有不同的签名

  • 使用Rename Method(函数改名)重命名函数,然后反复使用Move Method(搬移函数)将某些行为移入类,直到两者的协议一致为止。如果必须重复而赘余地移入代码才能完成这些,或许可运用Extract Superclass(提炼超类)为你自己赎点罪

19、Incomplete Library Class(不完美的库类)

  • 如果想修改库类的一两个函数,可以运用Introduce Foreign Method(引入外加函数)
  • 如果想加一大堆额外行为,运用Introduce Local Extension(引入本地扩展),好处"函数和数据被统一封装,使得其它类部分过分复杂"

20、Data Class(纯稚的数据类)

纯稚的数据类是指:它们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。这样的类只是一种不会说话的数据容器,它们几乎一定被其它类过分细琐地操控着。

  • 使用Encapsulate Field(封装字段)将这些字段封装起来
  • 如果这些类中有使用容器类的字段,使用Encapsulate Collection(封装集合)
  • 对于那些不该被其他类修改的字段,请运用Remove Setting Method(移除设值函数)

然后找到这些取值/设值函数的调用点。尝试以Move Method(搬移函数)把那些调用行为搬移到Data Class。如果无法搬移整个函数,可以采用Extract Method(提炼函数)后搬移。不久之后就可以运用Hide Method(隐藏函数)把这些取值/设值函数隐藏起来

21、Refused Bequest(被拒绝的遗赠)

子类不想继承父类某个函数或数据

  • 为这个子类新建一个兄弟类,再运用Push Down Method(函数下移)Push Down Field(字段下移)把所有用不到的函数下推给那个兄弟。这样一来,超类就只持有所有子类共享的东西。
  • 使用Replace Inheritance with Delegation(以委托取代继承)手法重构

22、Comments(过多的注释)

不是说不该写注释,是因为人们经常把注释当做"除臭剂"使用。常常会有这种情况:你看到一段代码有着长长的注释,然后发现,这些注释之所以存在仍是因为代码很糟糕。

  • 如果你需要注释来解释一块代码做了什么,试试Extract Method(提炼函数)
  • 如果函数已经提炼出来,但还需要注释来解释其行为,试试Rename Method(函数改名)
  • 如果你需要注释说明某些系统的需求规格,试试Introduce Assertion(引入断言)
  • 如果你还不知道该做什么,这才是引入注释的良好时机

戳码匠
0 声望0 粉丝