项目重构的个人经验

345 阅读6分钟

非常不幸地,在过去4、5年的时间里,我曾经主导过两个对极其混乱的项目代码的重构工作。从中也总结出一些粗浅的经验。

非必要不重构

重构前首先要谨记一点,重构最重要的不是能优化多少,而是不要出错。但代码重构本身的收益是不直观的且难以量化的,甚至有可能也不能提升系统性能,还容易出错,费力不讨好,因此一旦在重构过程中出错,会受到更大的压力。

在立项之前也需要让技术leader和业务leader都充分理解重构的必要性和复杂性。需要让大家对系统重构的过渡阶段和结果有充足的预期。

如果系统不是乱到无可救药的程度,不要轻易进行重构。

不要急着重构,先融入其中

“纸上得来终觉浅”。如果新接手这个项目,一定不能着急开始着手重构。只靠读代码、问同事、看文档,还是无法完全理解整个项目,更不要说现在面临的是一个非常非常乱的代码。基本熟悉之后,先开始跟着实际做一些业务类的需求,做一些小规模的代码清洁,比如规范命名、清理无用的注释。等过3 4个月之后,对业务已经有了足够的了解,再开始进行大规模重构。

借助伪代码梳理关键逻辑

这种混乱的代码里,一定少不了那种数千行的超大函数、几百行的大if-else、7 8层的大括号嵌套。里面还会充斥着各种陈年旧逻辑和各种笨拙的代码实现。想靠常人的智商应该很难完全读懂整个函数。

遇到这种单个的超大块复杂代码时,可以自己使用伪代码将这个函数重新复述一遍,复述的重点是理解代码里的大致关键逻辑。这个伪代码的语法细节可以完全凭自己习惯,不一定是标准的伪代码语法,比如我个人顺手的语法是混杂了python+Golang的; 也不需要准确里面用到的函数,能表达意义看懂即可。下面举几个例子:

// 比较复杂的对某个值的赋值逻辑
// 原代码: 
var n int
if cond1 == true && cond2 == true && value2 > 10 {
	n = 10
} else if con1 == false && cond3 == true && value3 > 100 {
	n = 20
} else if ... {
	n = 25
} else if ... {
	n = 30
} else ...

// 伪代码 
n = setValueN(...)  // 表示这里通过很复杂的逻辑确定了n的赋值,但具体的赋值过程暂时不重要

n = setN(cond1, cond2, cond3, value1, value2, value3, ...) // 重点标注出部分用到的变量
// 对某个值的赋值+修改+debug日志
// 原代码
users := make([]*model.Users, 0)
for _, u := range userData {
	users = append(users)
}
log.Infof("NumberOfUsers:%d, capOfUsersSlice:%d", len(users), cap(users))
for i, u := range users {
	if u.Country = "US" {
		users[i] = nil
		log.Infof("Set User to nil:%+v", u)
	}
}

// 伪代码
users = getUsers(userData) 
users.SetSomeNil()

整理逻辑表

所有业务逻辑都列成一张功能表,重构时对照着新代码的功能进行打勾。

标注异常逻辑

在熟悉代码时,可以把所有异常逻辑都专门收集到一张表格里。

异常逻辑指所有出乎意料、反直觉、反常理的逻辑或对某些特定的条件进行特殊处理的逻辑。也就是让你觉得“它出现在这里不合理”的代码。比如,一个名为 "makeData()"的函数里还包含了对某个字段的修改, 比如一个正在处理HTTP Response的函数里突然出现一个对HTTP Request里某个字段进行处理的代码。

特殊处理的代码, 比如推特“的当识别到发推人是Mask时,增加这条推的曝光度”。

每一个这样的异常逻辑背后,可能总有一个神奇的需求。

重新梳理领域模型

DDD思想在工程实践中有比较大的争议,但在重构时能发挥的作用却是毋庸置疑的。DDD难落地最大的原因就是在项目之初难以建立一个准确的领域模型,而在重构时,整个系统已经迭代了好几年,它的功能、需求、定位已经非常稳定,DDD里最难的问题已经天然地被解决了一大半。

按照当前系统的现状梳理出一个准确的领域模型,这个领域模型就是本次重构的最终目标。

步步推进

有些人可能希望两三个版本就完成全部的重构。这样可以清晰地比较出优化前后两个版本的代码质量差异,更能体现自己的厉害之处。但这样风险很大,难以观察和测试,也容易与中间加入的其它新功能冲突。

重构正确的节奏是小步快跑。不追求一步到位,先从小模块和结构优化开始,再慢慢扩展到大的模块重构。每个阶段都可以独立观察本次重构的效果。

哪怕由于原代码过于复杂,即使是单个函数或单个模块都无法一次性重构完成,也可以先把其中能重构的部分先做上,比如先从里面抽离出一个小函数,或者先把其中某一个for语句简化。这样也是一种熵减,是在降低整个系统的复杂度。最终熵减积累到一个临界值时,这个模块或函数已经足够简单了,就可以彻底完成一次性的重构。

具体到优化单个函数时,也可以步步推进。比如旧代码里多个不同的逻辑混杂在一个函数里,可以第一步先在原函数里把不同逻辑梳理开,拆分出来,相同逻辑聚集到一起,验证没问题后第二步再拆成多个函数。

“但这样我的能力就不好体现了。” 所以,“非必要不重构”。重构本身就是费力不讨好的事情。

AB & TDD

通过AB+TDD来确保重构中的每一步都是正确的。

在准备重构一个模块前,要先设计它的测试用例和ABTest方案。如果本来就有比较完善的测试用例就再好不过了。可按自己的业务场景设计一个专门用来重构的ABTest组件。

重构时,把旧代码复制一份,让新旧代码并联运转一段时间,对比AB结果。结果没问题再完全下掉旧代码。

代码层面的重构

一句话:参考《重构:改善既有代码的设计》。