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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yytoo2/article/details/87913034

背景(我也不知道这个算不算读书笔记,书本知识点整理和个人理解内容可跳至下面正文):

最近过年加找工作一直没想起来整理学习内容, 时间都很零碎。回想一下, 的确一直以来都是为了完成项目去看知识点。 除了刚开始想转行做IT的时候跟着慕课网的两条前后端路线系统地学习了一下, 但是终究囫囵吞枣。

前几天刚好有被问到《重构》这本书的内容。这两天临时抱佛脚看一下。 有了一定量的代码量积累的现在, 带着些许疑惑和朦胧的概念, 刚好到了看这本书的时候。

其实再上一家公司, 很早以前就被推荐这本书了, 不过不知道为什么后来忘了。 现在稍微看了下, 突然发现, 之前被要求我做到的一些代码习惯和公司内部的代码风格很多都能在书中找到。有些写代码的时候遇到的疑惑在书中也有提到(经常一边写新代码一边看之前的代码,尽量优化,奈何水平太菜, 经常迷茫于如何取舍,甚至最开始有段时间为了代码的简短和重复使用用力过头),感谢同事一直以来的帮助和容忍(我写的屎山和比蜗牛快不了多少的代码速度)。

在这个迷迷糊糊但是有点头绪的时候看这本书, 是最好的。

===========================================================================================

正文:

关于面向对象的五大原则:

  1. 单一职责: 一个类只负责一个职责。 目的在于解耦增强内聚。
  2. 开闭原则:对于扩展是开放的,对于修改是关闭的。可利用多态、抽象类等实现扩展,超类不应该做改变。
  3. 里氏替换原则:子类必能代替父类。
  4. 依赖倒转原则:高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象;抽象不应该依赖于具体实现,具体实现应该依赖于抽象
  5. 接口隔离原则:一个类对另一个类的依赖应该建立在最小的接口上,使用多个专门的接口比使用单一的总接口要好
  6. 迪米特原则(最小知识原则):一个对象应当对其他对象有尽可能少的了解。

所以, 在开发过程中, 我们的目标是:复用性、重构性、维护性。这本书很多地方都是以这三个目标为中心, 以求养成良好的代码风格和代码习惯。

----------------------------------------------------------------------------------------------------------

第一章:

这一章主要是用一个案例简单引出了一个重构的大概形象。

读这一章的时候我反思对比了一下以前开发过程中经历。如果有时间, 可以尝试下自己如何重构再对比下作者的做法

第一点我注意到的是, 重构需要以微小的步伐进行修改程序, 可以多次修改, 但是切忌不能一次性把一大段修改掉, 很容易出错。刚开始的时候我会犯这种错误, 心太急, 直接把整段代码拷贝出来, 有时候会把中间有些引入的参数或者被修改的参数(特别是Java传对象时引用传递,原对象会被不小心改掉)遗漏引发不必要的bug。所以在每一步的修改后都阶段性进行测试都是必要的。

所在在看到第一章的内容时如觉醍醐灌顶:在提取代码前, 首先我们必须 找出函数内局部变量和参数,未被修改的作为传入值, 被修改的作为返回值。 但是要注意的是被修改的参数尽量要少。

有一点之前我没想到过的是:只涉及某个类的方法, 写到该类下, 使用return返回该方法。 这个一般在第一遍写的时候就决定了代码放在哪个类, 但是在后期多次修改后的确会出现这个方法可能和其它类关系密切点的情况, 但是我不会去注意到需要移动他(也有一部分怕出错吧, 毕竟要是换了地方会引起其它地方拿不到, 越是到后期越是难以动这些代码, 涉及的东西太多, 修改非常容易出现遗漏, 工作量也很大)。

然后第一章的示例中还提到了变量的问题,感谢老大, 这方面之前没有问题,我们的代码中:

1. 引入参数以a开头,方法内变量名以类型首字母小写开头, 但是成员变量不加类型首字母, 自动生成get/set的方法时会出问题。

2. 类名采用名词,方法名采用动词加名词的格式,且不能以is/get/set开头,这些会和部分框架之类的冲突, 尽量避免。 类似增删改查固定功能的方法名会以特定单词和格式,所有代码统一。

3. 工具类的构造方法时静态的, 以防被实例化, 下面的方法采用静态方法。不同功能的工具类都归类整理,比如文件上传下载, world、excel文档处理放在一个工具类中,各类格式的转换(包括金额,英文时间处理)是一个工具类,文件和数据加解密是一个工具类, 这点符合书中说到的一个类只做一件事。 

4. 一些不便的参数等使用常量存储在单独的class中, 主要分两个class, 一个放配置方面的, 比如Ip地址等, 一个是放项目中用法到的, 比如不同交易方式对应的状态码。 统一采用大写, 使用下划线分割, 相同功能的多个参数开头部分有相同单词, 放在一起。

--------------------------------------------------------------------

第二章

重构含义:目的是不改变软件可观察行为的前提下,提高其可理解性,降低修改成本。

作者重复强调的是:重构之后软件功能一如既往, 其他程序员或用户是不会发现有些东西已经改变了。

开发时需要遵守的准则:

  • 添加新功能时,不应该修改既有代码,只管添加新功能并通过测试。
  • 重构时不再添加新功能,只管改进程序结构,并通过已有测试。

为何重构:

  • 重构改进软件设计(Design)
  • 重构使软件更容易理解(Maintain)
  • 重构帮助找到BUG(Debug)
  • 重构提高编程速度(Efficiency)

何时重构:

  • 三次法则:事不过三,三则重构
  • 添加功能时重构(New Feature)
  • 修补错误时重构(Bug Fix)
  • 复审代码时重构(Code Review)

何时不该重构:

  • 既有代码太混乱,且不能正常工作,需要重写而不是重构。
  • 项目接近最后期限时,应该避免重构。

重构的目标:

情况描述

目标

难以阅读的程序,难以修改

容易阅读

逻辑重复的程序,难以修改

所有逻辑都只在唯一地点指定

添加新行为时需要修改已有代码的程序,难以修改

新的改动不会危及现有行为

带复杂条件逻辑的程序,难以修改

尽可能简单表达条件逻辑

重构中的间接层:

间接层的作用(这里的几点达到的优化其实可以对应下一章的坏代码归纳):

允许逻辑共享(避免重复代码)
分开解释意图和实现(方法越短小,越容易起好名字揭示意图,单一职责)
隔离变化(软件需要根据需求的变化不断修改,隔离缩小修改的范围)
封装条件逻辑(多态消息)

正如作者所言, 间接层的使用需要注意层次多少处于恰当的范围。个人觉得之前有段时间有点过分追求避免重复代码而做不必要甚至负面的工作, 写完时候回发现不恰当的地方, 又改回去, 很浪费时间, 这大概就是我速度过慢的一部分原因。

-----------------------------------------------------------------------------------

第三章 代码的坏味道

这一章总结归纳了典型的需要重构的情况:

  1. Duplicated Code(重复代码):抽取方法到父类或者第三方类
  2. Long Method(过长函数):分解提炼函数, 以用途命名函数名
  3. Large Class(过大的类):遵循单一职责原则(SRP)。
  4. Long Parameter List(过长参数列表):常见的是将相关的参数组织成一个对象来替换掉这些参数。

这一点, 在之前的工作中, 比如前后端传输查询条件(一般我们的软件查询条件会很多)会采用类名是一个特定单词结尾的类的对象来存放。在后端方法中我可能会构造采用HashMap来存放, 如果有现成类可以用会放到一个对象中, 3个以下的就直接传多个参数。

  1. Divergent Change(发散式变化):因为不同的原因,在不同的方向上,修改同一个类。应该分解成更小的类,每个类只因一种原因而修改。

         这点回想下, 我的逻辑基本放在Service中了, SSM结构中Controller只做接受参数, 调用Service然后向前端返回, Mapper中只有sql语句, 很少涉及到逻辑, 有也只是一些简单的if或者日期之类格式转换。总共结构基本只有简单的这三部。

  1. Shotgun Surgery(霰弹式修改):每遇到变化,需要修改多个类,容易遗漏。把需要修改的部分放到一个类里。
  2. Feature Envy(依恋情结):函数大量地使用了另外类的数据。这种情况下最好将此函数移动到那个类中
  3. Data Clumps(数据泥团):两个类中相同的字段、函数签名中相同的参数等,都适合提取成一个单独的数据类。

             这点虽然看着简单易懂且容易入门, 但是在特定情况下可能迷惑性而且需要一定的技巧,在做的时候需要有条理, 详见第一章。

  1. Primitive Obsession(基本类型偏执):不要动不动就来个对象, 如果有一组经常被放在一起的数据, 可以让他们组成一个extract class ;如果在参数列中看到基本型数据, 尝试intoduce Parameter Object;如果发现自己正从数组中挑选数据, 可使用运用Replace Array with Object。
  2. Switch Statements(switch惊悚现身):少用switch,考虑用多态替换它。

这让我想到另一个, 有很多个简单if(根据条件返回一个值)连在一起的时候, 我会用HashMap, key为条件对应的状态值, value为结果。

  1. Parallel Inheritance(平行继承体系):如果发现每当为某个类增加一个子类,必须也为另外一个类相应增加一个子类, 这时需要考虑消除这种重复性。方法是让一个继承体系的实例引用另一个继承体系的实例。
  2. PLazy Class(冗赘类):如果一个类不值得存在,那么它就应该消失。
  3. Speculative Generality(夸夸其谈未来性):如果你的抽象类、委托、方法的参数没有实际的作用,那么就应当被移除掉。
  4. Temporary Field(令人迷惑的暂时字段):类中某个字段只为某些特殊情况而设置。把这些变量提炼到一个独立的类中。提炼后的新对象是个函数对象
  5. Message Chains(过度耦合的消息链):常常是因为数据结构的层次很深,需要层层调用getter获取内层数据。可以考虑这个字段是否应该移到较外层的类,或者把调用链封装在较外层类的方法。
  6. Middle Man(中间人):如果一个类的很多功能都通过委托给其他类来完成,那么就不如去掉这些中间人直接和真正负责的对象打交道。
  7. Inappropriate Intimacy(两个类过度亲密):
  8. Alternative Classes(异曲同工的类):根据用途重新命名,将某些行为移入类,或者使用超类类。
  9. Incomplete Lib Class(不完美的类库):修改类库的一两个函数 - 引入外部函数(Introduce Foreign Method);添加一大堆额外行为 - 添加本地扩展(Introduce Local Extension)
  10. Data Class(纯稚的数据类):数据类不应该把全部字段单纯的通过getter/setter暴露出来(我们在多层结构系统开发时经常这么做),而应该暴露抽象接口,封装内部结构。
  11. Refused Bequest(被拒绝的遗赠):a)子类继承父类的所有函数和数据,子类只挑选几样来使用:为子类新建一个兄弟类,再运用下移方法(Push Down Method)和下移字段(Push Down Field)把用不到的函数下推个兄弟类。b)子类只复用了父类的行为,却不想支持父类的接口:运用委托替代继承(Replace Inheritance with Delegation)来达到目的。
  12. Comments(过多的注释):适当注释, 不要太多, 代码中很明显的就不要注释了。但也不要一点都没有。

以上内容很多, 文中只有总结, 没有案例, 这个需要在不断加大代码阅读量和思考以及动手实践中不断去体会。

-------------------------------------------------------------------------

第四章 建立测试体系

// 之后再看, 之前测试只是简单地每个小功能模块完成后测试, 再拼装后整个流程测试。

----------------------------------------------------------------------------

第五章 重构列表

参数按照列表形式传递,检测重构每一个节点。工作中用Eclipse, 在开发过程中比较方便, 这一点可以得到控制。

----------------------------------------------------------------------

第六章 重新组织函数

函数不应过长, 传递的参数尽可能少而完整。 不要把传入的参数修改后返回。

猜你喜欢

转载自blog.csdn.net/yytoo2/article/details/87913034