前言
王健十六字心法:
旧的不变
新的创建
一键切换
旧的再见
古老的工程谚语:如果它还可以运行,就不要动它。
六个月后项目宣告失败:很大原因是代码太复杂,无法调试,也无法将性能调优到可接受水平。后来项目重新启动,几乎从头开始编写整个系统。
测试在重构中扮演非常重要的角色。
第一章 重构,第一个示例
-
常见的程序员立志: 应用于真实世界的程序中的有用技术。
-
重构的第一步永远相同:确保即将修改的代码拥有一组可靠的测试。(包括一套可靠的测试集)
-
回归测试是指修改了旧代码后,重新进行测试以确认修改没有引入新的错误或导致其他代码产生错误
- 自动回归测试将大幅降低系统测试、维护升级等阶段的成本。
-
提炼函数:将一块代码抽取成一个函数
-
首先要检查哪些变量会离开原本的作用域,并且更关心会被修改的变量
-
做完改动马上测试
-
提炼函数可以一步步消除过多的临时变量
-
-
编码风格:将函数返回值尽量命名为 result
-
编码风格:参数命名带上类型
-
营地法则:保证你离开时的代码库一定比来时更健康
-
第一章主要是举例子,代码挺多但是信息密度不大,初学者略微看看,主要建立一种典型的重构思维方式:
-
提炼函数
-
内联变量
-
搬移函数
-
以多态取代条件表达式
-
第二章 重构的原则
-
如果有人说"他们的代码在重构过程中暂时不可用"
- 那他们做的事不算重构
-
两顶帽子:添加新功能、重构
- 同时只戴一顶帽子
-
什么时候不应该重构:
-
有一段凌乱代码但是隐藏在一个 API 之下,它既没有出问题也不需要我马上理解它
-
另一种情况是重写比重构容易
-
-
重构与YAGNI
-
一些方法论经验之谈的总结
- 信息量不大
第三章 代码的坏味道
信息量较大,但是非常有用。基本上指明了需要重构的点
1 神秘命名
-
整洁代码最重要一环就是好的名字
- 函数、模块、变量、类的名字要能清晰表达自己的功能和用法
-
命名是编程中最难的两件事之一(很有名的梗,另一个是缓存失效)
-
要注意的是尽量不要用字符串替换的方式给函数或者变量改名字
-
可能遇到同名不同作用域,导致重名误改或者跨域漏改
-
尽量用 IDE 自带的重命名功能
-
2 重复代码
-
阅读重复代码:
-
设法将他们合而为一
-
留意细微差异,做成参数变量
-
-
代码复用节省二次修改的时间,并且避免漏改或者修改不一致的情况发生
3 过长函数
-
活得最长、最好的程序,其中的函数都比较简短
- 函数越长,就越难理解
-
小函数的间接性带来的好处:更好的阐释力、更易于分享、更多的选择
- 小函数易于理解的关键还是好的命名
-
早期的编程语言会为了极限性能尽量减少函数的使用。但是,现代编程语言已经几乎免除了进程内函数调用的开销
-
一个简单的原则:
- 每当我们需要以注释来说明点什么的时候,把需要说明的东西写到一个独立函数里,并以用途命名
-
常见的提炼函数信号:一段注释、条件表达式(switch)、循环
4 过长参数列表
-
虽然过长的参数列表不好,但也不要用全局变量来避免它
-
参数特别多并且经常一起出现, 考虑合并成一个对象或数据类
-
如果好几个函数经常用到相似度很高的参数列表, 考虑把多个函数合并成一个类的方法
-
如果同类参数经常共现, 使用数据类/值对象 来存放, 尽量使用不可变对象
-
5 全局数据
-
全局变量:
-
改成常量(其不可变)
-
多处可读,但只有一处可改
-
-
全局变量非常危险, 能不用尽量不要用, 变量的作用域越小越好, 变量的声明离它使用的位置越近越好
6 可变数据
-
函数式编程的思维建立在数据永不改变的概念基础上,如果要对某个对象改变,则返回一个修改后的副本。
-
纯函数要求函数运行时只依赖入参,并且执行过程中没有副作用(修改内部或外部变量),在并行编程等领域非常流行。
-
对于可变数据的修改,尽可能约束在一个地方,多个地方的修改容易导致困扰,将查询和修改(读与写)分离,尽可能缩小变量的作用域、缩短生命周期。
-
对于复杂结构的数据,尽可能使用值对象的方式进行操作,而不是数据的每个属性单独操作。(注意区分DDD中的值对象与实体对象)
7 发散式变化
-
清晰的上下文边界聚焦视野,清晰的数据结构用来通信(DTO),每次只关心一个上下文不受其他场景干扰。
-
需要较强的逻辑抽象能力,这里上下文边界和DDD限界上下文有所不同,这里更讲究独立性和边界固定。
8 霰弹式修改
-
如果某一次修改涉及到的代码散布多处,而且这些代码逻辑上又是有关联性存在的
- 那么考虑使用内联函数/内联类将这个本不该分散的逻辑关系合并到一起再修改
9 依恋情节
-
如果发现一个函数的调用相对于当前模块,更倾向于修改另一个模块对象里的数据,那么这个函数应该搬移到它本来应该在的地方(放错模块)。
-
简而言之:将总是一起变化的东西放在一块。
10 数据泥团
-
有一些总是绑在一起出现的数据项,他们值得新建一个类来存放
-
一般使用数据类处理这种逻辑关系,比如
-
尺寸=长+宽
-
坐标=x+y+z
-
-
尽管有时他们不是一起出现,会存在冗余情况
-
合并到一起的参数,可以缩短参数列表、简化函数调用
- 入参从 def func(a,b,c,d) 变成了 def func(Model):
11 基本类型偏执
-
不要过度迷信基本类型
-
比如 email 不只是字符串
- 里面还有隐含的信息,值得为它新建一个类来区分 name、domain
-
比如国际标准连续出版物号 ISBN
- ISBN = NewType(“ISBN”, str)
-
比如金钱类型使用简单的数字,会忽略单位导致对比大小时容易出错
- if CNY(100) > USD(100) 实际都是整型但单位不一样,不该直接比较
-
还有一个例子就是Python自带的 http 库里面的状态码相关对象
-
它们都是既包含整型数字部分也包含 message 字符串部分
-
更具语义性质的同时使函数调用更加准确合理
-
-
-
简而言之:如果有一组总是一起出现的基本类型数据,考虑提炼类和引入参数对象进行归纳。
12 重复的switch
-
使用多态替换大量的条件表达式(if、switch)
-
其实有些情况用 dict 选择回调函数也是可以的
13 循环语句
-
鄙视循环
-
管道操作(filter和map)比循环更能让人看到被迭代的元素和动作
14 冗赘的元素
-
可能有一个类,其实只有一个函数
- 这种情况类是多余的,只需要保留函数
-
前面提到过不要有太长的函数,这里也不建议有太短的函数
-
比如有个函数实际上它的名字跟代码实现看起来一模一样
-
则没有必要构造这么一个多余的函数
-
if a is True: return True
- 直接 return a
-
-
可能一开始要做更复杂的逻辑操作后来又被精简了
- 不要为可能不会发生的事情预留没有必要的冗余结构
-
15 夸夸其谈通用性
-
简而言之:不要提前做不必要的事情
- 不要高估通用性
-
比如一个抽象类实际上没有太大作用(没有约束或者重要意义)
- 则考虑折叠继承体系,避免一个类和超类没有太大区别却浪费继承
-
比如函数参数列表里的参数实际运行中没有被用上
- 则要么清理掉多余项,要么使用参数对象保留冗余
-
比如函数或类的唯一使用者是测试用例,而没有现实中的使用者
- 则这个函数或类就没有存在的必要
16 临时字段
-
前面提到过的减少使用临时变量
- 可以通过搬移函数给临时变量的逻辑创造一个家
-
所以不要滥用临时变量和全局变量
17 过长的消息链
-
简单的说,消息链类似下面的调用方式,大写字母X代表客户端,小写字母代表中间对象
-
a = X.get_a()
-
b = a.get_b()
-
c = b.get_c()
-
return c.do_sth()
-
消息链中的某一个环节如果发生改变,客户端需要重新排查并且做出相应修改,非常不安全也不方便。
-
-
简单做法是抽象出一个委托隐藏关系的函数,避免在外部一层层调用
- 调用方根本不关心实际依赖关系
18 中间人
-
过度使用中间人,过度使用委托
-
也就是一个类里大部分函数都是委托其他类实现的
-
则最好移除中间人直接和真正负责的类打交道
-
-
移除过多的中间人有点类似于去掉中介厂家直销
-
但是很多时候离不开中间人,所以不能完全不使用
- 抽象工厂、适配器、代理模式都是很好的抽象依赖
19 内幕交易
-
减少密谋,把多个类的数据交换放到明面上
-
比如 A 和 B 两个类原本是没有耦合的,但是由于业务需要加入了大量的数据交换场景
-
这时最好让二者在对象方法里面不要直接进行通信,而是单独实现一个通信类传入两个对象进行操作
-
参考 DDD 领域驱动设计里领域服务层(核心复杂业务)、应用服务层(数据传递或流程步骤)
-
-
20 过大的类
-
一个类如果做了过多的事情(方法过多),后期维护是非常吃力的事情,并且修改也非常危险。
-
如果有几个方法的前缀或者说做的事情关联性非常相似,则考虑提取出一个单独的类进行隔离。
- 有点类似 Mixin 模式
21 异曲同工的类
-
使做相似事情的不同的类使用相同的接口
- csv、json、yaml、toml 不同文件类型的配置导入都有 load、dump 方法
22 纯数据类
-
纯数据类简单理解就是:这个类有几个字段,然后有几个读写这些字段的方法,除此之外再无其他功能
-
使用纯数据类让事情更加纯粹
-
对某个数据类的具体业务逻辑上的操作可以使用其他类来实现
-
这样大量不同业务逻辑操作同样的数据对象时,做好了相对干净的解耦合
-
-
dataclass、typeddict、pydantic、msgspec
23 被拒绝的馈赠
-
子类继承父类的同名函数却不复用相同的接口(参数列表与返回类型)
-
则去掉无效的继承关系
-
例如父类函数 def parse(self, text: str) -> dict:
- 然而子类却是 def parse(self, text: list, mode: str, errors: str=“strict”) -> list:
-
24 注释
-
有时看到的一大段注释,往往是因为代码写的太难懂
- 重构解决可读性的同时,也消除了冗余的注释
-
如果不知道该做什么,则可以使用注释描述
- 为什么做某事、未来要做某事
第四章 构筑测试体系
-
测试驱动开发(Test-Driven Development,TDD)
-
内容太浅,随便找本TDD的书看。
第五~十二章 重构名录
-
基本都是上面坏味道修改方案相关的具体细节。没什么看头了。
-
结尾还有第三章的表格方便查询。
-
总体而言只要有了解过设计模式和软件开发各项原则,一看就明白了。
文章来源: https://wr5w8p13b8.feishu.cn/docx/WAD4deGz2ovJvAxSs6CcuXVBn2f