最近接手了一个老项目的代码重构工作,花了三周时间,把一个维护了五年、被无数人改过的"祖传代码",重构成了结构清晰、易于维护的代码。

这个项目最开始是一个人写的,后来陆续有五六个人参与过开发。每个人的编码风格不一样,技术水平也参差不齐,代码里充满了各种"坏味道":一个函数几百行、一个类什么都干、复制粘贴满天飞、注释和代码对不上、命名乱七八糟。

改第一周的时候,我几乎每天都在崩溃。但慢慢理清楚之后,通过一步步的重构,代码终于变得清晰了。这篇文章就来分享这次重构的经历,聊聊怎么从烂代码走到优雅代码。

顺便说一句,这次重构过程中,我也尝试了用AI工具辅助,比如用AI帮我理解代码、生成测试、建议重构方案。AI确实能提高效率,但核心的判断和设计,还是得靠人。

先搞清楚:什么是烂代码

在说怎么重构之前,先说说什么是烂代码。

烂代码不是说代码不能运行,而是说代码难以理解、难以修改、难以维护。它可能现在能跑,但每次加新功能或者修bug,都要花很长时间,而且很容易引入新的bug。

我总结了这个项目里最常见的几种"代码坏味道":

第一种是超长函数。一个函数写了两三百行,里面嵌套了五六层if-else,变量名起得像a、b、c、tmp1、tmp2。读这种函数,就像在走迷宫,读到后面忘了前面。

第二种是上帝类。一个类有几千行,什么功能都有,数据查询、业务逻辑、页面渲染、文件操作,全塞在一个类里。想改一个功能,得在这个类里翻半天,还怕改到别的地方。

第三种是复制粘贴。同样的逻辑,在五六个地方都有一份,只是变量名不一样。改一个bug,得改五六个地方,还经常漏改。

第四种是魔法数字。代码里到处是if (status == 3)、if (type == 7),你根本不知道3和7是什么意思,得去翻数据库或者问老员工。

第五种是注释过时。代码改了,但注释没改,注释说的是一回事,代码做的是另一回事。有时候看注释被误导,还不如不看。

第六种是嵌套过深。if里面套if,再套for,再套if,缩进都快到屏幕右边了。这种代码,逻辑稍微复杂一点就绕不清楚。

第七种是错误处理混乱。有的地方try-catch了但什么都不做,有的地方直接die(),有的地方返回false,有的地方抛异常。出了问题根本不知道哪里错了。

这些坏味道,单独看可能都不是大问题,但凑在一起,代码就变成了一个没人敢碰的"屎山"。

重构前的准备

重构不是上来就改,而是要先做好准备。

第一步是理解代码。我花了整整一周,把整个项目的代码读了一遍,画了模块关系图,理清楚了每个模块的职责和调用关系。这个过程很痛苦,但很重要。不理解代码就重构,等于盲人摸象,越改越乱。

第二步是建立测试。重构的前提是有测试保护,不然你改了代码,不知道有没有改坏。这个项目原来几乎没有测试,我先给核心模块写了单元测试和集成测试。有了测试,重构的时候就有了安全网。

第三步是制定重构计划。我把重构分成了几个阶段:先改命名和格式(风险最低),再拆分超长函数和上帝类,再消除重复代码,最后优化架构和设计。每个阶段完成后,跑一遍测试,确保没有问题。

第四步是和团队沟通。重构不是一个人的事,要和团队成员沟通,告诉他们我在做什么、为什么要做、预计多长时间。同时,在重构期间,尽量减少新功能的开发,避免代码冲突。

这些准备工作,花了我将近一周的时间。但磨刀不误砍柴工,准备做好了,后面的重构才会顺利。

第一阶段:命名和格式

重构的第一阶段,是改命名和格式,这是风险最低的改动。

我做了几件事:

第一,统一代码风格。用代码格式化工具(比如PHP-CS-Fixer、Prettier)把整个项目的代码格式统一了,缩进、空格、换行、括号位置,全部统一。这一步虽然不改变逻辑,但能让代码看起来整齐很多,读起来也舒服。

第二,改善命名。把a、b、c、tmp1、tmp2这些无意义的变量名,改成有意义的名字。比如把$a改成$userList,把$b改成$totalCount。函数名也要改,把doSomething()改成getUserOrderList()。

第三,消除魔法数字。把代码里的3、7、1这些数字,改成常量或者枚举。比如把status == 3改成status == Order::STATUS_PAID,这样一看就知道是什么意思。

第四,更新注释。把过时的注释删掉或者更新,让注释和代码一致。同时,删除那些废话注释,比如// 给变量赋值 这种毫无意义的注释。

这一阶段的改动,虽然简单,但效果很明显。代码读起来顺畅多了,理解成本降低了不少。

第二阶段:拆分超长函数和上帝类

第二阶段,是拆分超长函数和上帝类,这是重构的重点。

先说拆分超长函数。我的方法是:先理解函数的整体逻辑,然后找出其中的独立逻辑块,把它们提取成小函数。比如一个处理订单的函数,里面有验证参数、查询用户、计算价格、创建订单、发送通知这几块,我就把每一块都提取成独立的函数,主函数只负责调用。

提取函数的时候,我遵循几个原则:每个函数只做一件事,函数名要清晰地描述它做什么,函数参数不要太多(最好不超过3个),函数长度控制在20行以内。

拆分之后,原来两三百行的函数,变成了一个十几行的主函数,加上几个小函数。读起来就清晰多了,主函数一眼就能看出整体逻辑,想看细节再去看小函数。

再说拆分上帝类。我的方法是:根据职责把大类拆成多个小类。比如原来的Order类,里面有订单查询、订单创建、订单支付、订单退款、订单通知这些功能,我就把它拆成OrderRepository(数据查询)、OrderService(业务逻辑)、OrderNotifier(通知)这几个类,每个类只负责一块。

拆分的时候,要注意类之间的依赖关系,尽量减少耦合。可以用依赖注入,让类之间通过接口依赖,而不是直接依赖具体实现。

这一阶段的改动比较大,风险也比较高。每拆一个函数或类,我都会跑一遍测试,确保功能没有问题。有时候拆完之后发现逻辑有问题,还得回退重新来。但拆完之后,代码的结构清晰了很多,维护起来也容易了。

第三阶段:消除重复代码

第三阶段,是消除重复代码。

这个项目里重复代码很多,同样的逻辑在很多地方都有。我的方法是:先找出重复的代码块,然后提取成公共函数或者公共类,让所有需要的地方都调用同一个。

比如,好几个地方都有"查询用户信息"的逻辑,我就把它提取成UserRepository的getUserById方法,所有地方都调用这个方法。以后如果查询逻辑变了,只需要改一个地方。

消除重复代码的时候,要注意不要过度抽象。有时候两段代码看起来很像,但业务含义不一样,这时候强行合并,反而会让代码更复杂。我的原则是:重复三次以上,才考虑提取;如果只是两次重复,可以先放着。

还有一种重复是条件判断的重复。比如很多地方都有if ($user->isVip()) { ... } else { ... },我会把这个判断封装成一个方法,比如$user->getDiscount(),让调用方不用关心具体的判断逻辑。

这一阶段完成后,代码量减少了不少,逻辑也更统一了。以后改一个逻辑,不用到处找重复的地方了。

第四阶段:优化架构和设计

第四阶段,是优化整体的架构和设计。

前面三个阶段,都是在现有架构的基础上改善代码质量。这一阶段,要思考架构层面的问题:模块划分是否合理?依赖关系是否清晰?是否有更好的设计模式?

我做了几件事:

第一,引入分层架构。把代码分成了Controller层(接收请求)、Service层(业务逻辑)、Repository层(数据访问)、Model层(数据模型)。每一层只负责自己的事情,层与层之间通过接口调用。这样,代码的职责更清晰,修改某一层不会影响其他层。

第二,使用设计模式。在合适的地方引入了设计模式,比如用工厂模式创建对象,用策略模式处理不同的支付方式,用观察者模式处理事件通知。设计模式不是为了炫技,而是为了解决特定的问题,让代码更灵活、更易扩展。

第三,统一错误处理。把原来混乱的错误处理,统一成了异常处理机制。所有的错误都抛异常,由统一的异常处理器来捕获和处理。这样,错误信息更清晰,排查问题也更容易。

第四,优化数据库访问。把原来拼接SQL的方式,改成了用查询构造器或者ORM。同时,加了索引优化,减少了慢查询。数据库访问的性能提升了不少。

这一阶段的改动最大,也最考验设计能力。我花了一周多的时间,才把架构调整好。但改完之后,整个项目的代码结构焕然一新,加新功能的时候,终于不用在"屎山"里翻找了。

AI辅助重构的体验

这次重构,我也尝试了用AI工具辅助,说说体验。

AI最有用的地方是理解代码。遇到一段看不懂的老代码,我会把它贴给AI,让AI解释这段代码在做什么。AI通常能很快给出解释,比自己一行行读快多了。当然,AI的解释不一定完全准确,需要自己判断。

AI也能帮我生成测试。给AI一段函数代码,让它生成对应的单元测试,AI能生成大部分测试用例,我只需要补充一些边界情况。这比自己手写测试快很多。

AI还能给重构建议。我会把一段烂代码贴给AI,问它"这段代码有什么问题,怎么重构"。AI通常能指出一些问题,比如函数太长、重复代码、命名不好,也能给出一些重构方案。这些建议有参考价值,但最终怎么改,还是得自己决定。

但AI也有局限。它不了解整个项目的上下文,给出的建议有时候不符合项目的实际情况。它也不能替代人的设计判断,比如怎么拆分模块、怎么设计接口,这些还是得靠人。

我的体会是:AI是一个很好的辅助工具,能提高重构的效率,但不能替代人的思考。用AI的时候,要把它当成一个"结对编程的伙伴",而不是"替你写代码的工具"。

重构的效果

三周之后,重构基本完成了。说说效果。

第一,代码量减少了。原来的代码有三万多行,重构之后变成了两万行左右。减少的主要是重复代码和无用代码。

第二,代码可读性提高了。函数平均长度从原来的50多行,降到了15行左右。类的平均长度从原来的300多行,降到了100行左右。命名更清晰,结构更合理。

第三,维护成本降低了。加一个新功能,原来需要两三天,现在一天就能搞定。修一个bug,原来要翻半天代码,现在很快就能定位到问题。

第四,bug减少了。重构之后,因为代码结构更清晰,测试更完善,新引入的bug明显减少了。

第五,团队士气提高了。以前大家都不想碰这个项目,觉得是"屎山"。重构之后,代码变优雅了,大家也愿意改了,开发效率提高了不少。

当然,重构也不是没有代价。花了三周时间,期间新功能开发几乎停了。而且重构过程中,也引入过几个bug,虽然测试拦住了大部分,但还是有一两个漏网之鱼。但总体来说,收益远大于成本。

重构的经验和教训

这次重构,我也总结了一些经验和教训。

第一,重构之前一定要有测试。没有测试保护的重构,就是赌博。我见过太多人,重构完之后功能全乱了,就是因为没有测试。

第二,小步快跑,不要一次改太多。每次只改一个小地方,改完就跑测试,确认没问题再改下一个。不要想着"一次重构到位",那样风险太大。

第三,重构和新功能不要混在一起。重构的时候,尽量不要加新功能;加新功能的时候,尽量不要重构。混在一起,出了问题不知道是重构的问题还是新功能的问题。

第四,不要为了重构而重构。重构的目的是让代码更易维护,而不是追求完美的设计。如果一段代码很稳定,从来不需要改,那就没必要重构它。重构那些经常需要改的、问题最多的代码,投入产出比最高。

第五,重构是持续的,不是一次性的。不要想着"重构完就一劳永逸了"。代码会不断变化,坏味道会不断产生。要把重构当成日常开发的一部分,每次改代码的时候,顺手改善一下周围的代码。

第六,和团队保持沟通。重构不是一个人的事,要让团队成员知道你在做什么,理解重构的价值。否则,别人可能会觉得你在"瞎改",不配合你。

写在最后

代码重构,是每个程序员都会遇到的事情。

烂代码不可怕,可怕的是不敢面对烂代码,或者用错误的方式重构。只要方法得当,烂代码也能变成优雅代码。

这次重构让我深刻体会到:好的代码不是写出来的,是改出来的。第一版代码可能很粗糙,但通过不断的重构,它会变得越来越好。

也让我体会到:代码是写给人看的,顺便让机器执行。写代码的时候,要想着下一个读这段代码的人,可能就是半年后的自己。写清晰、易维护的代码,是对自己和团队负责。

如果你也在面对一堆烂代码,不要害怕。先理解它,再建立测试,然后一步步重构。相信我,当你把烂代码改造成优雅代码的时候,那种成就感,是写新代码比不了的。

希望这篇文章能给正在做重构的朋友一些参考。如果你有什么重构的经验或者问题,欢迎交流。