做AI绘画商业化项目的人大概都有类似经历。一开始为了赶进度,代码怎么快怎么写,功能跑起来就行。等用户量上来了,问题就全暴露了。改一个bug引出三个新bug,加个功能要翻半天代码,性能也开始扛不住。

我们的项目就是这样走到必须重构的地步。这篇文章记录整个过程,希望能给有类似困扰的人一些参考。

重构前的烂摊子

代码乱到什么程度呢?一个接口函数写了八百多行,里面混着参数校验、业务逻辑、数据库操作和返回格式化。复制粘贴的代码到处都是,同样的图片处理逻辑在三个地方各写了一遍,还略有不同。

最头疼的是bug。用户反馈生成的图片有时候方向不对,查了半天才发现是因为有个地方图片旋转处理写反了。修好之后过两天又有人说另一个接口有同样的问题,原来复制的时候把bug也复制过去了。

性能也不行。高峰期接口响应要好几秒,数据库连接经常打满。查了一下,发现很多查询没加索引,还有的地方在循环里查数据库,一次请求下来查了几十次。

重构前的准备

决定重构之后,没有急着动手。先做了几件准备工作。

第一件事是补测试。原来的代码几乎没有测试,重构的时候没有测试兜底,改完根本不知道有没有改坏。我们花了一周时间,给核心接口写了集成测试,保证重构前后行为一致。

第二件事是梳理代码结构。画了一张依赖关系图,才发现模块之间的依赖乱成了麻。A模块引用B,B又引用A,还有的工具类里写了业务逻辑。理清了这些,才知道从哪里下手。

第三件事是制定计划。不追求一次改完,而是分模块逐步重构。每个模块改完就跑测试,确认没问题再继续。这样即使中途有新需求插入,也不会被打断太多。

重构的几个原则

整个重构过程我们坚持了几个原则。

第一个是小步快跑。每次只改一个小地方,改完立即测试提交。从来不搞"大爆炸"式的重构,那种改完一堆文件然后发现跑不起来的经历太痛苦了。

第二个是保持功能不变。重构阶段只改结构,不加新功能,也不改业务逻辑。这样出了问题很容易定位,就是重构引入的,不会和需求变更混在一起。

第三个是持续集成。每次提交都自动跑测试,有问题立刻发现。重构期间我们的CI比平时盯得更紧,从来不让红灯过夜。

具体做了什么

首先是拆函数。把那些几百行的大函数按职责拆成小函数,每个函数只做一件事。拆完之后代码可读性大幅提升,以前看一个函数要翻三屏,现在一眼就能看明白在干什么。

然后是消除重复。把重复的代码提取成公共函数,统一维护。图片处理、参数校验、错误返回这些常用逻辑都抽到了工具类里。后来再遇到类似问题,改一个地方就全解决了。

接下来是分层。把代码分成控制层、业务层和数据层。控制层只负责接收请求和返回响应,业务逻辑放在业务层,数据库操作封装在数据层。分层之后各层职责清晰,改业务逻辑不用碰接口定义,改数据库不用动业务代码。

最后是性能优化。给慢查询加了索引,把循环里的数据库查询改成批量查询,热点数据加了缓存。优化之后接口响应时间从平均三秒降到了三百毫秒,数据库连接也不再紧张了。

重构后的变化

最直观的变化是代码好维护了。新人入职看代码不再一脸懵,改bug也不用提心吊胆。上次加一个新功能,从评估到上线只用了两天,放在以前至少要一周。

bug也少了很多。重复代码消除之后,同类问题不会再到处出现。测试覆盖率提上来之后,很多潜在问题在提交的时候就被发现了。

团队的心情也好了。以前面对一堆烂代码,大家都不想碰,能绕就绕。现在代码结构清晰,改起来有成就感,反而愿意主动优化了。

几点教训

回头看整个过程,有几点教训值得记下来。

不要一开始就追求完美。项目早期快速验证想法是对的,代码糙一点没关系。但要知道什么时候该停下来还债,不能等烂到动不了了才想起重构。

重构要及时。代码刚有点坏味道的时候就收拾,成本最低。等到积重难返的时候,重构的风险和成本都会高很多。

测试是重构的安全网。没有测试的重构就是赌博,运气好没事,运气差就线上出故障。哪怕只给核心路径写测试,也比完全没有强。

最后,重构不是一次性能做完的事。代码在不断变化,今天的优雅代码明天可能又变丑了。把重构当成日常习惯,随时顺手收拾,就不会再积累到需要大动干戈的地步。