重构-改善既有代码的设计 笔记
文章目录
重构 改善既有代码的设计(Refactoring Improving the Design of Existing Code) 笔记
重构(refactoring)可以定义为,在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。也可以简单的说,重构是在代码写好后改进它的设计。
第1章 例子
如果要添加一个特性,而代码结构使你没法简单的做到,就先重构那个程序,再添加特性。
测试之前,先要有一套测试机制,这些测试必须有自我检验的能力。
第一步,找出代码的逻辑泥团,并运用 Extract Method,比如把 switch/if 等语句中的代码提炼到独立的函数中。
PS: 只是添加新的函数,这种重构有何意义?除非一个函数长度超过了屏幕,或者逻辑明显不同,或者能代码共享,否则就算函数稍微长一点,只要逻辑缜密,放在一起可能会更好,尤其是那种有顺序的步骤。要知道,新函数就像新线程导致状态切换一样,函数过多会增加复杂度,阅读代码还会不停的看来看去。
先从代码中找出函数的局部变量和参数。不被修改的变量可以直接作为参数,会被修改的变量,需要注意,一般要作为返回值。
(PS: 其实直接把要分出的代码复制粘贴到新函数即可,借助IDE提示,一眼就能看到需要什么参数,确实要注意会被修改的值)
然后,某些意思不清楚的变量名和函数名也需要修改。完成每一步,都要记得运行测试。
分出新函数后,可能发现函数没有用到自身类的成员,而是用到了参数类的成员。多数情况下,函数应该放到它所使用的数据的所属类中。这里需要用到 Move Method。
PS: 这里体现了一些 Extract Method 的作用。
然后,可能需要删除多余的变量,就是那些只声明了一次,但后面没修改过的。这里用 Replace Temp with Query 的思路。尽量去掉这些临时变量,它们会导致变量被传来传去,阅读时容易跟丢,还使得代码冗长。
PS: 注意函数中对某一个变量的计算,这种计算很可能需要用 Extract Method 分到一个新函数中。为了去掉这种临时变量,或者为了共享一部分代码,也可以用这种方法。
PS: 之前写角色技能时,代码中充斥这很多 if 条件判断,最后的重构方法就是使用角色状态机,分出多个角色状态类,每个状态处理自身的逻辑,就去掉了状态判断,也使得新逻辑添加更简单。
最好不要在别的对象的属性上运用 switch 语句,应该是在对象自己的数据上使用。如果 Move Method 时发现需要传递自身参数,即一个方法需要多个类的成员,应该把方法放到哪个类中?我觉得可以看方法属于哪个类,书上说应该放到可能会修改的类中。
如果某一个类要分多种情况,比如一部影片有普通价格、儿童价格、新版价格,原来的代码使用 switch 在 Movie 类中来获取不同的价格。 考虑分出三个 Movie 的子类,缺点是这些个子类对象无法在生命周期中修改自己所属的类,所以这里用 State/Strategy 模式比较合适。即抽象出一个价格 Price 类,然后实现它的三个子类用于计算不同的价格,原来在 Movie 持有的价格字段改为一个价格对象。首先可以用 Replace Type Code with State/Strategy 把类型相关的行为搬到 State 模式内,即新建几个类专门用于不同的价格计算,然后用 Move Method 将 Switch 语句移到一个 Price 基类中,最后用 Replace Conditional with Polymorphism 去掉 switch 语句。
PS:一部Movie有个Price属性,当然不是简单的通过这个属性分类为不同的子类,如果这样做,那么再多一个2D电影或3D电影属性,那又要继续分类吗?这些Movie的属性,如果比较复杂需要拆分,就只能去拆分这个属性本身,所以就多一个Price组件,通过Price基类来统一代码逻辑,让子类实现具体的计算。如果属性比较简单,switch语句根本不需要去掉。
第2章 重构原则
重构,通过调整代码内部结构,在不改变外部可观察行为的基础上,提升代码的可理解性,降低修改成本。
消除重复代码,是优秀设计的根本。
程序设计就是告诉计算机做什么,计算机精确的执行每一行代码。在重构上花一点时间,让代码很好地表达自己的用途,核心就是“准确说出我所要的”。
我是个懒惰的程序员: 总是记不住自己写过的代码。事实上,对于任何能够立刻查阅的东西,我都不故意去记住它。我总是把该记住的东西写进程序里,这样就不必记住它了。
重构开始总是趋于一些细枝末节,等代码变得简洁后,可以容易的看到之前看不到的设计层面的东西。
重构不但可以改善代码质量,还能提升开发速度,良好的设计使添加和修改变得容易。
重构的时机,你可以随时随地的重构,让代码更容易理解。最常见的时机就是添加新功能时,为了帮助理解要修改的代码进行重构。重构的原动力是,代码的设计使我无法简单的添加新特性。
遇到bug修补错误时,也要考虑是否重构,能否很容易看出bug。
难以阅读的程序,难以修改 逻辑重复的程序, 所有逻辑在唯一的地方指定 添加新行为需要修改已有代码的程序 带复杂条件逻辑的程序,难以修改
通过增加 间接层 来解决设计,但是会多出很多东西,增加了复杂度,需要综合权衡,好处:
允许逻辑共享,比如分出一个子类 分开解释意图和实现 隔离变化,比如怕影响原来的,新建子类 封装条件逻辑
过早发布接口,常常带来维护接口的烦恼,尤其是你权限不足,无法修改接口的时候。
PS: 上面一个固定的主英雄列表,下面一个滚动的副英雄列表。点击英雄图片,之前选中的可以与当前点击的互换位置。已经知道当前点击的是主还是副,现在要知道前一个选中的是主或副。原来我是添加一个英雄变量记录选中的英雄,然后在主英雄列表中查询,这样就知道是否为主英雄。我感觉不如直接在记录选中英雄的同时,增加一个维度变量,记录它属于主或副就行了,这样可以避免稍复杂的查询。在涉及到前后变量记录时,如果信息不够,添加变量即可,注意当前记录的就是下一次调用时之前的。最后我发现,查询操作很简单,就没有做这种修改了。 现在项目的问题是,写功能前一定要先写接口,几乎每个组件类都要关联一个接口,再加上一堆注释,给人感觉很繁琐,而且还要先有服务端的接口,客户端的接口再继承接口,这样就搞出了一堆相似功能的东西,看着非常繁琐。这些所谓的组件按理说是为了复用,定义这么多接口是为了啥?这些组件目前也没有被复用,项目一开始就这样写,不清楚是为了啥。
如果现有代码根本不能运作,不应该重构,应该重写,这样反而简单。
如果项目已经到了最后期限,重构就没必要了。
编程前首先要进行设计,得到一个合理的解决方案,然后再进行实际编码,完成后再重构。预先设计是非常有必要的。这种预先设计,再重构的办法,使得编程得到了简化,即不必花费大量时间为了灵活性进行复杂设计,毕竟如果这些灵活性后面没有用就真的是浪费精力。当下只需要构建可运行的最简化系统就行了。
第3章 代码的坏味道
(1)重复代码,必要的时候可以独立出一个类,需要用时持有一个类的对象即可。
(2)过长的函数,给小函数一个好名字,这样就不用看具体写了什么。不再于函数的长度,在于函数做什么和如何做之间的语义距离。寻找注释,注意代码用途和实现手法之间的语义距离。除了注释的地方,条件,循环常常也是需要提炼代码的地方。
PS: 做什么和如何做常常作为分割代码要考虑的地方。框架中,Task 要管理很多东西,所以要尽量简单,很多时候只需要表明做什么,如何做的部分常常要交给 Controller 去做。
(3)过大的类,相关变量和方法可以分到一个新类中来做。
(4)过长的参数列,用传递对象来替代
(5)发散式变化,修改只应该在单一的类中,如果做不到这一点,应该提炼出一个新的类。
(6)散弹式修改,要修改的地方很多,这种情况应该用 Move Method 或 Move Field 把要修改的集中在一起。
(7)依恋情节,函数对某个类的兴趣高于对自身类的兴趣。这种情况,直接把函数放到它所属的类即可。函数应该放到它所使用的数据最多的类中,这样大部分数据都不必公开。根本原则是,将总是一起变化的东西放在一块儿。
(8)数据泥团,总是在一起的数据项或者函数参数应该拥有它们自己的对象。
(9)基本类型偏执,小任务上也可以运用小对象
(10)switch语句,使用 replace type code with subclass 去掉它,或者用 replace parameter with explicit methods 替代。条件判断的问题在于重复,常常发现很多地方存在相同的判断,添加新功能时要修改所有这些地方。如果只在单个地方做判断,就没有这个问题。
(11)平行继承体系,每当为一个类增加一个子类,也需要为另一个类增加子类。
(12)冗赘类,如果一个类不值其身价,就让它消失吧。维护每个类都是有成本的。
(13)夸夸其谈未来性,和上面类似。目前用不到的抽象类,不必要的委托,没用的函数的参数, 过于抽象的函数名等。
(14)令人迷惑的暂时字段。某些字段只在某个算法中像参数一样使用,此时可以把变量和函数提炼到一个独立的类中。
(15)过度耦合的消息链,一旦任何函数发生修改,很多地方都需要修改。可以在适当的地方增加函数来缩短消息链。或者可以观察对最终获得的对象做了什么,然后提炼出一个独立函数,然后直接使用这个独立函数。
(16)中间人,某个类接口有一般的函数都委托给其他类,这就是过度运用委托,可以去掉中间人,直接和负责的对象打交道。
(17)狎昵关系,两个类过于亲密,探究彼此的私有成员。应该采用移动方法或者字段让他们划清关系,或者把相同部分提炼出新的类。
(18)异曲同工的类,做的事相同,却有不同的签名。
(19)不完美的库类
(20)纯粹的数据类,如果是外部在操作这些数据,就应该把操作部分提炼到数据类中
(21)被拒绝的遗赠,子类只想用继承来的少数方法怎么办
(22)过多的注释。当你感觉需要注释时,先重构,让注释变得多余。
第4章 构筑测试体系
在写代码之前,先写测试,使得你更注重接口而非实现,这永远是一个好事。
单元测试是高度局部化的,测试某一个接口是否工作正常
功能测试是完全面向用户的,把系统当成一个黑箱,只要保证功能正常即可
容易出现的 bug,应该编写单元测试来暴露这些 bug,这类似常见的错误 Log
测试无法捕捉所有 bug,也不用费太多时间写出所有测试,发现大多数 bug 就够了
第5章 重构列表
第6章 重新组织函数
要么使用 Extract Method 提炼出新函数,要么用 Inline Method 去掉没用的函数。
遇到过长的函数,或者一段需要注释才好理解的代码,可以用 Extract Method 提炼函数。简短而命名良好的好处: 函数的粒度变小,复用的几率更大,高层函数读起来就像注释,不必仔细看具体的小函数。函数的长短不重要,在于函数的语义和名称之间的距离。
(1) 提炼函数:根据函数意图来命名,以它 “做什么”来命名,而不是“怎样做”。 如果遇到对局部变量赋值,如果该变量只在这个函数中使用,把变量声明移动到新函数即可; 如果修改了变量,可以新建一个有返回值的函数,遇到需要返回多个参数的情况可以分出多个函数。
(2) 内联函数:在调用点插入本体,移除原函数。如果某些简短的函数,内部逻辑很清晰易读,可以考虑去掉这些间接函数;
(3) 内联临时变量。如果临时变量妨碍了重构,就去掉它。
(4) Replace Temp with Query,以查询取代临时变量。将表达式放到一个独立的函数中,用新的函数替换表达式。把临时变量替换为查询,方便共享逻辑。
(5) 引入解释性变量 用临时变量来分解一些复杂的计算过程。这里还可以使用 Extract Method 的方法。
(6)分解临时变量 某个临时变量被赋值超过一次,既不是循环变量,也不被用于收集计算结果。 针对每次赋值,创造一个独立、对应的临时变量。 PS:因为这其实应该是多个独立变量,没必要用相同变量名。
(7)移除对参数的赋值 在函数中,可以修改传递进来的参数的状态,但是最好不要修改参数指向的对象,在C#中一般用in参数来做。这样做不好的地方在于,混用了按值传递和按引用传递,容易出错。较好的办法是,把要修改的参数赋值给一个临时变量,然后修改临时变量,最终把临时变量的值返回或者赋值给要修改的参数。
(8)以函数对象取代函数 存在一个大型函数,其中的局部变量让你无法 Extract Method。可以将这个函数放入一个单独的对象中,其中局部变量成为字段,然后可以在这个对象中把大型函数分解为多个小型函数。这样改动的原因在于,小型函数更加具有可读性。
(9)替换复杂的算法为简单的算法
第7章 在对象之间搬移特性
文章作者 huijian142857
上次更新 2021-08-22