摘要:何为不健康的代码
DuplicatedCode(重复代码)
- 一个以上地点看到相同的程序结构:提炼新函数
- 同一个类的两个函数含有相同的表达式::提炼新函数
- 两个互为兄弟的子类内含有相同表达式:分别提炼代码,推入超类。不完全相同的,将相似部分和差异部分分割
- 两个毫不相关的类:考虑方法提取、继承
LongMethod(过长函数)
- 活的好,活的长
- 程序愈长愈难以理解
- “间接层”所能带来的全部利益:解释能力、共享能力、选择能力,都是由小型函数支持的
- 让小函数容易理解的真正关键在于一个好名字
- 更积极地分解函数
- 原则:
- 当需要以注释来说明点儿什么的时候,将需要说明的东西写进一个独立函数中,并以用途(非实现手法)来命名
- 可以对一组甚至一行代码做这件事儿,哪怕替换后的函数调用动作比函数自身还长,只要函数名称能够解释其用途,也该毫不犹豫地这么做
- 关键不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离
- 99%的场合里,要把函数变小,只需使用方法提取:找到函数中适合集中在一起的部分,将它们提炼出来,形成一个新的函数
- 如果函数内有大量的参数、临时变量,会对函数提炼形成阻碍。
- 避免将许多参数、临时变量当作参数传递给被提炼出来的新函数,导致可读性几乎没有任何提升
- 运用replace temp with query来消除这些临时元素
- introduce parameter object和preserve whole object,则可将过长的参数变得简洁
- 如果还是有太多临时变量、参数,则使用杀手锏:replace method with method object
- 如何确定该提炼哪一段代码?
- 寻找注释:
- 可以指出代码用途、实现手法之间的语义距离。
- 注释是在提醒你,可将这段代码替换成一个函数,且可以在注释的基础上为这个函数命名
- 就算只有一行代码,如果需要以注释来说明,也值得将其提炼到独立函数去
- 条件表达式、循环:
- 使用decompose conditional处理条件表达式
- 循环,将循环和其内的代码提炼到一个独立函数中
- 寻找注释:
large class 过大的类
- 缺点:拥有太多实例变量、拥有太多代码
- 解决:
- extract class:将几个变量一起提炼至新类内。提炼时,应该选择类内彼此相关的变量。通常,如果类内的数个变量有着相同的前缀、字尾,一位着有机会把它们提炼到某个组件内。
- extract subclass:同理,适合作为一个子类。
- 提炼方法:代码重复、混乱
- extract interface:确定客户端如何使用它们,为每一种方式提炼出一个接口
- 独立领域对象:如果是个GUI类,可能需要把数据、行为移到一个独立的领域对象去
- duplicate observe data:两边各保留一些重复数据,并保持两边同步。
long parameter list 过长参数列
- 把函数所需的所有东西,都以参数传递进去。除此之外只能选择全局数据,而全局数据是邪恶的东西
- 对象技术改变了这一情况:如果手上没有所需的东西,总可以叫另一个对象给你
- 有了对象,只需要给它足够的、让函数能从中获得自己所需的东西就行了。
- 函数需要的东西多半可以在函数的宿主类中找到。
- 面向对象中的函数,参数列通常比在传统程序中短很多
- 缺点:
- 难以理解,太多参数会造成前后不一致,不易使用;
- 一旦需要更多数据,不得不修改它。如果将对象传递给函数,大多数修改都没有必要。
- 解决:
- replace parameter with method:如果向已有对象发出一条请求就可以取代一个参数。已有对象:可能是类内的一个字段,也可能是另一个参数
- preserve whole object:将来自同一对象的一堆数据收集起来,并以改对象替换它们
- introduce parameter object:如果某些数据缺乏合理的对象归属,为它们制造一个“参数对象”
- 例外:不希望造成”被调用对象“与”较大对象“间的某种依赖关系,这时将数据从对象中拆解出来,单独作为参数也很合理。但需注意其所引发的代价。如果参数列太长、变化太频繁,就需要重新考虑自己的依赖结构了
divergent change 发散式变化
- 易修改,相关性尽量减少
- 一个类受多种变化的影响
- 解决:
- divergent change:某个类因为不同的原因在不同的方向上发生变化。
- extract class:针对某一外界变化的所有相应修改,都只应发生在单一类中,而这个新类内的所有内容都应反应此变化。应该找出某特定原因而造成的所有变化
- 目的:使“外界变化”与“需要修改的类”趋于一一对应
shotgun surgery 散弹式修改
- 如果每遇到某种变化,都必须在许多不同的类内做出许多小的修改
- 需要修改的代码散步四处,不但很难找到它们,也很容易忘记某个重要的修改。
- 一种变化引发多个类相应修改
- 解决:
- move method、move field:将所有需要修改的代码放进同一个类。
- inline class:把一系列相关行为放进同一个类,可能会造成少量divergent change,但可以轻易处理
- 目的:使“外界变化”与“需要修改的类”趋于一一对应
feature envy 依恋情节
- 将数据、对数据的操作行为,包装在一起
- 解释:函数对某个类的兴趣搞过对自己所处类的兴趣,数据依恋
- 解决:
- move method:将函数移到应该去的地方
- extract method + move method:函数中有一部分受依恋之苦
- 一个函数往往会用到几个类的功能,究竟该被置于何处?判断哪个类拥有最多被此函数使用的数据。可先用extract method将函数分解为数个较小函数,并分别置于不同地点
- 根本原则:
- 将总是一起变化的东西放在一起。
- 数据和引用这些数据的行为总是一起变化的,也有例外
- 如果例外出现,就搬移那些行为,保持变化只在一地发生
data clumps 数据泥团
- 很多地方看到相同的三四项数据:两个类中相同的字段;许多函数签名中相同的参数
- 绑定在一起出现的数据,应该拥有数据它们自己的对象
- 解决:
- extract class:将一起出现的数据,提炼到一个独立对象中
- introduce parameter object、preserve whole object:将注意力转移到函数签名上。将很多参数列缩短,简化函数调用
- 不必在意Data clumps上只用上新对象的一部分字段,只要以新对象取代两个、更多字段,就值回票价。
- 判断依据:删掉众多数据中的一项,其他数据没有因而失去意义。如果它们不再有意义,则是明确信号,应该为它们产生一个新对象。
- 好处:
- 减少字段、参数个数
- 一旦拥有新对象,就可以着手找寻feature envy,帮你指出能够移至新类中的种种程序行为
primitive obsession 基本类型偏执
- 结构类型数据:将数据组织成有意义的形式
- 基本类型数据:构成结构类型的积木块
- 结构总会带来额外的开销,可能代表着数据库中的表,如果只为做一两件事而创建结构类型也可能显得麻烦。
- 对象的最大价值:模糊了横亘于基本数据、体积较大的类之间的界限。可以轻松编写出与语言基本类型无异的小型类
- 新手通常不愿意在小任务上运用小对象
- 解决:
- replace data value with object:将原本单独存在的数据值替换为对象,从而走出传统的窟窿,进入炙手可热的对象世界
- 例如:结合数值和币种的money 类;由起始值、结束值组成的range类;电话号码或邮政编码……特殊字符串
- replace type code with class、replace type code with state/strategy:有与类型码相关的条件表达式
- extract class:有一组总是被放在一起的字段
- intruduce parameter object:参数列看到基本型数据
- replace array with object:从数组中挑选数据
- replace data value with object:将原本单独存在的数据值替换为对象,从而走出传统的窟窿,进入炙手可热的对象世界
switch statements switch代码块
- 少用switch、case语句。本质上都是在重复
- 可用多态来优雅解决
- switch语句长根据类型码进行选择,要的是“与该类型码相关的函数、类”
- 解决:
- 多态
parallel inheritance hierarchies 平行继承体系
- 特殊的shotgun surgery
- 每当为某个类增加一个子类,必须为另一个类相应增加一个子类
- 特征:某个继承体系的类名称前缀与另一个继承体系的类名称前缀完全相同
- 解决:
- 让一个继承体系的实例引用另一个继承体系的实例&&move method、move field。将引用端的继承体系消弭于无形
lazy class 冗赘类
- 所创建的每一个类都需要有人去理解它,维护它,这些工作都是要花钱的
- 如果一个类的所得不值其身价,就应该消失
- 场景:
- 某个类原本对得起自己的身价,但重构使其缩水,不再承担那么多的责任
- 开发者事前规划了某些变化,并添加一个类来应付这些变化,但变化实际没有发生
- 解决:
- collapse hierarchy:如果某些子类没有足够的工作
- inline class:几乎没用的组件
speculative generality 夸夸其他未来性
- “我想总有一天需要做这事儿”。企图以各式各样的钩子、特殊情况来处理一些非必要的事情。
- 造成系统更难理解、维护
- 如果所有装置会被用到,就值得这么做;如果用不到,就不值得。用不上的装置只会挡你的路,所以,把它搬开吧
- 解决:
- collapse hierarchy:某个抽象类没有太大作用
- inline class:除去不必要的委托
- remove parameter:函数的某些参数未被用上
- rename method:函数名称带有多余的抽象意味
- 测试用例:如果函数、类的唯一用户是测试用例,则为speculative generality。如果发现这样的函数、类,请把它们连同测试用例一并删除。但如果它们的用途是帮助测试用例检测正当功能,必定刀下留人。
temporary field 令人迷惑的暂时字段
- 某个实例变量仅为某种特定情况而设。
- 这样的代码让人不易理解。通常认为对象在所有时候都需要它的所有变量
- 在变量未被使用的情况下,猜测当初的设置目的,会让人发疯的
- 解决:
- extract class:把所有和这个变量相关的代码,都放进这个新家
- 如果类中有一个复杂算法,需要好几个变量,往往导致temporary field的出现。
- 由于实现者不希望传递一长串参数,所以将这些参数都放进字段中。
- 但这些字段只有使用改算法时才有效,其他情况只会让人迷惑
- extract class:将这些变量和其相关函数提炼到一个独立类中,提炼后的新对象将是一个函数对象
- intruduce null objec:变量不合法的情况下,创建一个null对象,避免写出条件式代码
- extract class:把所有和这个变量相关的代码,都放进这个新家
message chains 过度耦合的消息链
- 消息链:对象传递性请求
- 特征:代码中可能是一长串的getthis{}或一长串的临时变量
- 意味着客户代码将于查找过程中的导航结构紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出相应的修改
- 解决:
- hide delegate:在消息链的不同位置进行这种重构手法。
- 理论上,可以重构消息链上的任何一个对象,但这么做往往会把一系列对象intermediate object都变成middle man
- 通常,更好的选择是,先观察消息链最终得到的对象是用来干什么的,看看能否使用extract method把使用该对象的代码提炼到一个独立函数中,再运用move method将这个函数推入消息链
- 如果这条链上的某个对象有多位用户打算添加此消息链的一部分,就加一个函数来做这件事
- 注意:不是任何函数链都是坏味道
- hide delegate:在消息链的不同位置进行这种重构手法。
middle man 中间人
- 过度运用委托:某个类有一半的函数都委托给其他类
- 解决:
- remove middle man:直接和真正负责的对象打交道
- inline method:如果这样“不干实事”的函数只有少数几个,将其放进调用端。
- replace delegation with inheritance:如果middle man 还有其他行为,将其变成实责对象。即可扩展原对象的行为,又不必负担那么多的委托动作
inappropriate intimacy 过度亲密
- 两个类过于亲密,花费太多时间去探究彼此的private成分。
- 对于类,希望尽量保持独立
- 继承往往造成过度亲密:子类对超类的了解总是超过超类的主观愿望
- 解决:
- move method、move field:划清界限,减少亲密度
- change bidirectional association to unidirectional:让其中一个类脱离另一个类
- extract class:将两者共同点提炼到一个安全地点,并使用新类
- hide delegate:让另一个类来传递关系
- replace inheritance with delegation:继承造成的过度亲密,让子类独自生活,离开继承体系
alternative classes with different interfaces 异曲同工的类
- 解决:
- rename method:两个函数做着同一件事,有着不同的签名。根据用途为其重新命名
- remove method:将某些行为移入类,直到两者的函数功能不同为止
- extract superclass:防止重复而冗余的移入代码
incomplete library class 不完美的类库
- 库作者没有未卜先知的能力&&需求不明确,导致库往往构造的不够好,往往不可能让我们修改其中的类来完成我们希望完成的工作。
- 解决:
- introduce foreign method:如果只想修改库类的一两个函数
- intruduce local extension:想要添加一大堆额外的行为
data class 纯粹的数据类
- 拥有一些字段,以及用于访问(读写)一些字段的函数。除此之外,一无是处
- 这样的类,只是一种不会说话的数据容器,几乎一定被其他类过分细碎地操控着
- data class就像一个小孩子,起点很好,但若要让它们像成熟的对象那样参与整个系统的工作,它们就必须承担一定责任
- 解决:
- encapsulate field:类中早期拥有public字段。在别人注意之前将其封装起来
- encapsulate collection:如果类内含容器类字段,且没有得到恰当的封装
- remove setting method:对于不该被其他类修改的字段
- move method:找出取值、设值函数被其他类运用的地点,将调用行为搬移到data class 中
- extract method:无法搬移整个函数,则残生一个可搬移的函数。
- hide method:将取值、设值函数隐藏起来
refused bequest 被拒绝的继承
- 子类应该继承超类的函数、数据。
- 如果子类不想、不需要继承所有的函数、数据,仅需要其中的一部分?
- 不建议的传统做法:(起码不建议每次都这么做)
- 为子类新建一个兄弟类
- push down mehtod、push down field 把所有用不到的函数下推给那个兄弟
- 最终,超类只持有子类共享的东西。达到“所有的超类,应该是抽象的”
- 经常利用继承来复用一些行为,并发现可以很好地应用于日常工作。但这也是一种坏味道,但气味通常不那么强烈
- 建议:
- 如果refused bequest引起困惑。问题,请遵循传统忠告,但不必每次都这么做。十有八九这种坏味道很淡,
- 如果子类复用了超类的行为(实现),却不愿意支持超类的接口,refused bequest的坏味道就会变得浓烈。
- 拒绝继承超类的实现,可以不介意;但如果拒绝继承超类的接口,必须重视。
- replace inheritance with delegation:即使不愿意继承接口,也不要胡乱修改继承体系。
comments 过多的注释
- comments不是一种坏味道,是一种好味道
- 但,人们常用Comments来作为除臭剂。
- 常见情境:一段代码,有长长的注释。而注释之所以存在,是因为代码很糟糕。
- comments可以带我们找到上面所提到的各种坏味道。找到坏味道之后,用各种重构手法去除坏味道。完成之后,常发现:注释已经变得多余了,因为代码已经说清楚说明了一切
- 解决:
- extract method:注释来解释一块代码做了什么
- rename method:如果函数已经提炼出来,但还是需要注释来解释其行为
- introduce assertion:如果需要注释说明某些系统的需求规格
- 建议:
- 当感觉需要撰写注释时,请先尝试重构,试着让所有的注释都变得多余
- 注释的良好运用情境:(信息可以帮助将来的修改者,尤其是那些健忘的人)
- 不知道该做什么:用来记录将来的打算
- 标记并无十足把握的区域
- 写下自己“为什么做某某事”