天天看点

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

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

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

重构的定义

在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。本质上说重构就是在代码写好之后改进它的设计。

第一章 重构,第一个案例

1、作者以一个影片出租店用的程序,计算每一位顾客的消费金额并打印详单,来重构程序,告诉我们发现痛点果断重构。

如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。

2、重构前,先检查自己是否有一套可靠的测试机制,这些测试必须有自我检验能力。

3、更改变量名是绝对值得的行为,好的代码应该清楚表达出自己的功能,变量名是代码清晰的关键。

任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员 。

第二章 重构原则

1、两顶帽子:添加新功能和重构。首先尝试添加新功能,然后意识到如果把程序结构改一下,添加新功能会方便很多,于是你换顶帽子,做一会重构。然后把帽子换回来,继续添加新功能,如此循环往复。

2、重构改进软件设计。良好的设计是快速开发的根本。

3、重构使软件更容易理解。

4、重构帮你提高编程速度。

5、重构的时机:不要安排固定的时间进行重构,而是开发阶段随时随地进行重构。

三次法则:第三次再做类似的事情,你就必须要重构了。

添加新功能的时候是最常见的重构的时机。

6、重构的难题:

  • 数据库的结构很难改变,一旦改变意味着你不得不迁移数据。
  • 修改接口
  • 对于已经发布的接口需要可能需要维护旧接口和新接口,用 deprecated (不建议使用)修饰旧接口;

7、重构与设计二者不是互斥的关系,而是不同的项目阶段不同的选择、重点。

8、性能优化放在开发的后期,通过分析工具找出消耗大量时间空间的地方,然后集中精力优化这些地方;

第三章 代码的坏味道

1、坏味道首当其冲的就是Duplicated Code,idea现在都会对duplicate code默认给出警告。最单纯的duplicate code就是同一个类的两个函数或者两个互为兄弟的子类内有相同的表达式,这时就需要提炼出重复的代码,然后都调用被提炼出来的代码。

2、Long Method(过长函数)

3、Large Class(过大的类)

4、Long Parameter List (过长的参数列表)

5、发散式变化

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

6、霰弹式修改

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

7、依恋情节

如果一个函数中只有部分代码出现这种依恋,那么就把那部分独立成一个函数再迁移。

8、数据泥团

两个类中相同的字段,函数中相同的参数。这些一起出现的数据应该拥有属于他们自己的对象。

9、基本类型偏执

可以尝试着把2个类组成一个小类,这样就有机会把修改函数也放入这个小类。

10、switch惊悚现身

可以考虑用多态来替换switch

11、平行继承体系

让一个继承体系的实例引用另一个继承体系的实例。

12、Lazy Class冗赘类

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

13、夸夸其谈未来性

如果实际用不到,就把这些抽象的设计去掉。

14、令人迷惑的临时字段

如果不希望传递一长串参数,把这些变量和相关函数提炼到一个独立类中。

15、过度耦合的消息链

代码结构紧密耦合,一旦对象间的关系发生变化,客户端就需要改。

16、middle man 中间人

人们可能过度运用委托。应该remove middle man,直接和真正负责的对象打交道。

17、Inappropriate intimacy狎昵关系

有时两个类过于亲密,经常访问对方的private变量。我们需要把共同点提炼到一个安全的地方供两个类使用。

18、异曲同工的类

如果两个函数做同一件事情,却有不同的签名,请运用rename method重新命名。

19、不完美的库类

如果你只想修改库类的一两个函数,可以Introduce foreign method,如果要添加一大堆额外行为,就用Introduce Local extension。

20、data class纯稚的数据类

尝试把get和set方法的调用代码搬移到data class,这样你就可以用hide method把这些函数隐藏起来。

21、被拒绝的遗赠

如果子类复用了超类的实现,却又不愿意支持超类的接口,坏味道就会变得浓烈。

22、过多的注释

找到坏味道并用重构把坏味道去除之后,我们会发现注释已经变得多余了,因为代码已经说明了一切。

第四章 构建测试体系

价值:一套测试就是一个强大的bug侦测器 能够大大缩减查找bug所需要的时间。

Junit 测试框架的运用

每当你收到bug报告,请先写一个单元测试来暴露bug。

第五章 重构列表

记录格式:名称,概要,动机,做法,范例

第六章 重新组织函数

1、extract method提炼函数

提炼函数遇到的麻烦的问题是 有局部变量,对局部变量再赋值。

2、inline method 内联函数

有时候你遇到某些函数,内部代码和函数名同样清晰易读,你就应该去掉这个函数。

例外一种情况是有一群组织不合理的函数,你可以将它们都内联到一个大型函数中,再从中提炼出合理的小型函数。

3、inline temp 内联临时变量

内联临时变量多半是作为replace temp with query的一部分使用的,如果这个临时变量妨碍了其他的重构手法,比如extract method,就应该将它内联化。

4、replace temp with query 以查询取代临时变量

多创建几个函数,把所有临时变量都替换为查询。

5、introduce explaining variable 引入解释性变量

在条件逻辑中,可以将每个条件子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的意义。

6、Split Temporary Variable 分解临时变量

有很多临时变量用于保存一段代码的运算结果。如果它们被赋值超过一次,说明承担了一个以上的责任,应该被替换为多个临时变量。

7、remove assignments to parameters 移除对参数的赋值

在java中,不要对参数赋值,那会混淆了值传递和引用传递。

8、replace method with method object 以函数对象取代函数

考虑新建一个类,把所有局部变量变成新类的字段,然后把函数的代码复制过来,源函数改为调用新类的同名方法,这个新类的实例就称为method object,新类里面就可以做extract method了。

9、substitute algorithm 替换算法

可以考虑替换掉原来的算法。不过在进行该重构前,确认你对原算法非常了解。

第七章 在对象之间搬移特性

1、Move method 搬移函数

如果一个类有太多行为,或者与另一个类有太多合作形成高度耦合,就可以搬移函数。

2、move field 搬移字段

对于一个字段,另一个类有更多函数使用了它,就可以考虑搬移这个字段。

如果是public的字段,考虑先封装起来。

3、extract class 提炼类

类变得过分复杂,此时你需要考虑哪些部分可以分离出去,形成一个单独的类。某些数据和函数总是一起出现,那么它们就应该分离出去。

还有一个extract class的信号:如果你发现子类化只影响类的部分特性,或者某些特性需要以另一种方式来子类化,这就意味着你需要分解原来的类。

4、inline class将类内联化

如果一个类不再承担足够责任,挑选这一萎缩类的最频繁用户,以inline class手法将萎缩类塞进另一个类中。

5、hide delegate 隐藏委托关系

如果某个客户先通过服务对象的字段得到另一个对象,然后调用后者的函数,那么客户就必须知晓这一层委托关系。万一委托关系变化,客户也得相应变化。可以在服务对象上放置一个委托函数,将委托关系隐藏起来。这样即使委托关系发生变化,变化也被限制在服务对象中,不会波及客户。

6、remove middle man 移除中间人

如果频繁使用委托,会导致委托函数越来越多,服务类完全变成了一个中间人,此时你就应该让客户直接调用受托类。

7、introduce foreign method引入外加函数

如果你在多处需要这段代码,就应该抽成一个函数来调用,避免重复代码。

8、 Introduce Local Extension 引入本地扩展

如果引入了很多外加函数,就新建一个类来包括这些函数,可以用子类化和包装的标准对象技术来做。

第八章 重新组织数据

1、自封装字段

间接访问变量,代码比较容易阅读,子类可以通过覆写一个函数而改变获取数据的途径。

2、以对象取代数据值

3、将值对象改为引用对象

引用对象:每个对象对应真实世界中的一个实物,比如客户对象,账户对象。

值对象:对象是完全由值来定义,比如日期,钱。

4、将引用对象改为值对象

值对象有个非常重要的特性:它们应该是不可变。

5、以对象取代数组

人们很难记住“数组的第一个元素是人名”这样的约定。如果你用对象就不一样,你可以运用字段名称和函数名称来传达这样的信息。

6、复制被监控数据

将该数据复制到一个领域对象中,建立一个observer模式。

7、将单向关联改成双向关联

两个类需要使用对方特性,需要添加双向连接。

8、将双向关联改成单向关联

两个类的其中一个类不再需要另一个类的特性,去除不必要的关联。

9、以字面常量取代魔法数

魔法数:拥有特殊意义,却不能明确表现出这种意义的数字。这些数字发生改变,就必须在程序中找到所有魔法数修改,噩梦。

解决方案:声明一个常量来表示魔法数。

10、封装字段

将字段声明为private,并提供相应的访问函数。

11、封装集合

返回集合的只读副本,可以返回一个集合的复制给client。可以提供集合的增删方法,但不要提供set集合的方法。

12、以数据类取代记录

你可能面对一个遗留的用非面向对象语言写的程序,或者从数据库读出的记录。创建一个数据类,以便日后在类中都有一个对应的字段。

13、以类取代类型码

用Enum。

14、以字段取代子类

如果子类只是用来返回常量数据,那就可以消除它,避免继承带来的额外复杂性。

15、以state/strategy取代类型码

16、以字段取代子类

第九章 简化条件表达式

1、分解条件表达式

如果有复杂的条件语句,从if、else段落中分别提炼出独立函数。

2、合并条件表达式

如果有一系列条件测试,都得到相同结果,合并成一个条件表达式,并提炼成一个独立函数。

3、合并重复的条件片段

将重复代码搬到条件表达式之外。

4、移除控制标记

以break或者return代替标记

5、以卫语句取代嵌套条件表达式

条件表达式:用if else嵌套的条件表达式。

卫语句:if之后马上return,如果某个条件极其罕见,就应该单独检查该条件,并在条件为真时立刻从函数中返回。这样的单独检查被称为卫语句(guard clauses)

6、以多态取代条件表达式

条件表达式根据对象类型的不同而选择不同的行为。将每个分支放进一个子类的覆写函数,将原始函数抽象。

7、引入Null对象

反复地判断一个对象是否为null是非常繁琐的。空对象需要有一个能被识别出是空对象的方法,比如isNull()。

8、引入断言

断言不是用来检查“你认为应该为真”的条件,它是用来检查“一定必须为真”的条件的。

第十章 简化函数调用

1、函数改名

将复杂的处理过程分解成小函数,但要明确小函数的用途关键就是给函数起一个好名字。

2、添加参数

你必须修改一个函数,修改后的函数需要 一些过去没有的信息,所以你需要添加参数。

3、删除参数

当一个参数不再需要的时候,删除它。

4、将查询函数和修改函数分离

5、令函数携带参数

函数做着类似的工作,只是因为少数几个值导致行为略有不同,你可以把那几个值用参数来表示,把两个函数合并成一个。

6、以明确函数取代参数

跟上面的优化相反。

7、保持对象完整

有时候,你会将来自同一个的对象中取出若干数据作为参数,将它们作为某次函数调用。如果把对象传给函数,以防将来增减函数参数。

8、以函数取代参数

如果函数可以通过其他途径获得参数值,那么就不应该通过参数取得该值。过长的参数列会增加理解难度。

9、引入参数对象

当你把这些参数组织到一起后,往往会发现一些可被移至新建类的行为。调用函数一般会对这一组参数有一些共通的处理,如果把这些共通行为移到新对象中,可以减少很多重复代码。

10、移除设值函数

如果类的某个字段在对象被创建的时候被设值,然后就不再改变,去掉该字段的所有设值函数。

11、隐藏函数

函数没有被用到,设置private

12、以工厂函数取代构造函数

13、封装向下转型

Downcast

14、以异常取代错误码

返回特定的代码改成异常。

15、以异常取代异常

异常不应该被滥用。它只应该被用于异常的、罕见的行为,不应该成为条件检查的替代品。

第十一章 处理概括关系

1、字段上移

两个类拥有相同的字段 将该字段移至超类

2、函数上移

两个函数,在各个子类中产生完全相同的结果,将该函数移至超类

3、构造函数本体上移

在子类的构造函数中调用它

4、函数下移

5、字段下移

6、提炼子类

类中的某些特性只是部分实例用到,移到子类。

7、提炼超类

两个类有相似特性,移至超类

8、提炼接口

若干客户使用类接口中的同一个子集

9、折叠继承体系

超类和子类无太大区别

10、塑造模块函数

你有一些子类,其中相应的某些函数以相同顺序执行类似的操作,但操作的细节不同。

11、以委托取代继承

超类中的一些函数你不需要,这时需要新建一个子类委托继承你需要的函数,然后再继承你需要的类

12、以继承取代委托

两个类之间使用委托关系,并经常为整个接口编写许多简单的委托函数。

第十二章 大型重构

1、梳理并分解继承体系

2、将过程化设计转换为对象设计

3、将领域和表述/显示分离

4、提炼继承体系

第十三章 重构,复用与现实

现实中的重构。