最近,我接手了一个AI电影项目的代码重构工作。

这个项目,是之前一个团队做的,功能已经上线了,能跑,但代码质量非常差。用"烂代码"来形容,一点都不为过。

接手这个项目的时候,我心里是崩溃的。代码没有任何架构可言,所有的逻辑都写在一个文件里,一个函数有几百行,变量名都是a、b、c、temp,注释几乎没有,重复代码到处都是,硬编码满天飞。每次要加一个新功能,或者修一个bug,都要花好几天时间才能看懂代码。而且改了一个地方,另一个地方就出问题,牵一发而动全身。

我花了大约一个月的时间,对这个项目进行了彻底的重构。重构之后,代码变得清晰、优雅、可维护、可扩展。加新功能、修bug的效率,提高了好几倍。

这篇文章,我想分享一下这个AI电影项目的代码重构经历,聊聊我是怎么从烂代码一步步重构到优雅代码的,以及在重构过程中的一些思路、方法和经验。如果你也在维护烂代码,或者想学习如何写出优雅的代码,希望这篇文章能给你一些启发。

先简单介绍一下这个项目。这是一个AI电影生成平台,用户可以输入文字描述,AI自动生成短视频/电影。主要功能包括:用户管理、项目管理、剧本生成、角色生成、场景生成、视频生成、任务调度、支付系统等。技术栈是Python + FastAPI + MySQL + Redis + Celery。

烂代码长什么样

先给大家看看,这个项目的烂代码,到底有多烂。

第一个问题,没有架构,所有逻辑都写在一个文件里。

整个项目的后端逻辑,几乎都写在一个叫main.py的文件里。这个文件有5000多行代码,用户管理、项目管理、剧本生成、视频生成、支付,所有的逻辑都在这里面。没有分层,没有模块化,所有的东西都混在一起。

你要找用户相关的代码,得在5000行里搜;你要找视频生成的代码,也得在5000行里搜。每次看代码,都像在大海捞针。

第二个问题,函数过长,一个函数做太多事情。

项目里有一个叫generate_video的函数,有800多行代码。这个函数做了所有的事情:验证用户权限、检查用户余额、创建任务、生成剧本、生成角色、生成场景、生成视频、保存结果、扣减余额、发送通知。所有的逻辑,都在这一个函数里。

你要改视频生成的逻辑,得在800行里找;你要改余额扣减的逻辑,也得在800行里找。而且,这个函数里有大量的嵌套if-else,缩进有七八层,看的时候,你都不知道自己在哪个分支里。

第三个问题,变量名不规范,看不懂是什么意思。

项目里的变量名,都是a、b、c、d、temp、data、result、info。你根本不知道这些变量是什么意思,要靠猜。

比如,有一段代码:

a = get_user(b)
c = a['d']
if c > 100:
    e = True
else:
    e = False

你能看懂这段代码在干什么吗?我反正看不懂。后来我花了半天时间,才搞明白,a是用户对象,b是用户ID,c是用户余额,d是余额字段,e是判断用户余额是否充足。

第四个问题,没有注释,代码的意图不明确。

整个项目,注释加起来不到50行。而且,这些注释大多是没用的,比如# 初始化变量# 循环# 返回结果。真正解释代码意图、业务逻辑的注释,几乎没有。

你看一段代码,不知道它为什么要这么写,不知道它解决的是什么问题,不知道有什么业务规则。只能靠猜,靠调试,靠问之前的开发人员(如果还能联系上的话)。

第五个问题,重复代码到处都是。

同样的逻辑,在不同的地方写了好几遍。比如,检查用户余额的逻辑,在视频生成、图片生成、音频生成三个地方,各写了一遍。而且写法还不一样。获取用户信息的逻辑,在十几个地方各写了一遍。

重复代码的问题是,你要改一个逻辑,得改好几个地方。而且很容易漏改,导致有的地方改了,有的地方没改,出现不一致的bug。

第六个问题,硬编码满天飞。

项目里有大量的硬编码:API地址、密钥、价格、阈值、文件名、路径,都直接写在代码里。比如,视频生成的价格是10块钱,直接写在代码里:price = 10。AI服务的API地址,直接写在代码里:url = "http://xxx.com/api"

硬编码的问题是,要改的时候,得改代码,重新部署。而且,不同环境(开发、测试、生产)的配置不一样,硬编码很容易导致环境混乱。

第七个问题,没有错误处理。

项目里几乎没有错误处理。调用AI服务的API,不检查返回值,不处理异常,直接就用。如果AI服务挂了,或者返回了错误,代码就直接崩溃了,用户看到的是500错误,而不是友好的提示。

数据库操作,也不处理异常。如果数据库连接失败,或者SQL执行出错,代码就直接崩溃了。

第八个问题,没有日志。

项目里几乎没有日志。出了问题,你不知道是哪里出了问题,只能靠猜,靠复现。尤其是AI视频生成这种异步任务,出了问题,你根本不知道任务执行到哪一步了,是哪一步出了错。

这些问题加在一起,导致这个项目的代码,几乎不可维护。每次加新功能、修bug,都是一场噩梦。

重构的思路

面对这样的烂代码,我没有一上来就重写。重写的风险很大,很容易写出新的bug。而且业务逻辑很复杂,重写很容易遗漏一些细节。

我采用的是渐进式重构的思路:在不改变外部行为的前提下,逐步改善代码的内部结构。

具体来说,我的重构思路是:

第一,先理解代码,再动手。

在重构之前,我花了大约一周的时间,仔细阅读代码,理解业务逻辑。我把每个功能的流程都画了出来,把每个函数的作用都写了注释,把每个变量的含义都搞清楚了。

只有真正理解了代码,你才能安全地重构。否则,你改了代码,可能会改变原来的行为,引入新的bug。

第二,建立测试,确保行为不变。

在重构之前,我先给核心功能写了测试。虽然项目原来没有测试,但我还是花了一些时间,给最核心的功能(比如视频生成、支付)写了集成测试。

有了测试,我在重构的时候,就可以随时运行测试,确保我的重构没有改变原来的行为。如果测试通过了,说明重构是安全的;如果测试失败了,说明我改坏了,需要回滚或者修复。

第三,从小处着手,逐步重构。

我没有一上来就重构整个项目,而是从一个小的模块开始,比如用户管理模块。把这个模块重构好了,测试通过了,再重构下一个模块。

从小处着手的好处是,风险小,每次重构的范围有限,出了问题也容易定位和回滚。而且,每完成一个小模块的重构,都会有成就感,能激励你继续下去。

第四,重构和新功能分开。

在重构期间,我尽量不加新功能。重构和新功能混在一起,很容易出问题,也很难测试。如果必须加新功能,我会先把新功能加在旧代码里,等重构到这个模块的时候,再一起优化。

第五,持续提交,随时可以回滚。

重构的时候,我会频繁地提交代码,每完成一个小的重构,就提交一次。这样。如果出了问题,可以随时回滚到上一个正常的版本,不会丢失太多工作。

重构的具体步骤

有了思路之后,我开始了具体的重构工作。以下是我重构的主要步骤:

第一步,分层架构,模块化。

我做的第一件事,是把原来那个5000多行的main.py,按照功能拆分成不同的模块,建立分层架构。

我采用的是经典的三层架构:

  • 表现层(API层):处理HTTP请求,参数校验,返回响应。对应原来的路由和视图函数。
  • 业务逻辑层(Service层):处理核心业务逻辑,比如视频生成、支付、用户管理。对应原来的业务逻辑代码。
  • 数据访问层(Repository层):处理数据库操作,增删改查。对应原来的数据库操作代码。

除了这三层,我还加了一些公共模块:

  • models:数据模型,对应数据库表。
  • schemas:请求和响应的数据结构,用Pydantic定义。
  • utils:工具函数,比如日期处理、字符串处理、加密解密。
  • config:配置管理,从环境变量读取配置。
  • exceptions:自定义异常。
  • tasks:异步任务,用Celery定义。

拆分之后,原来的main.py,只剩下了路由的注册,不到100行。每个模块,都有清晰的职责,代码结构一目了然。

第二步,拆分大函数,单一职责。

接下来,我把那些几百行的大函数,拆分成小函数,每个函数只做一件事,遵循单一职责原则。

比如,原来那个800多行的generate_video函数,我拆分成了以下几个小函数:

  • validateuserpermission:验证用户权限
  • checkuserbalance:检查用户余额
  • create_task:创建任务
  • generate_script:生成剧本
  • generate_character:生成角色
  • generate_scene:生成场景
  • generate_video:生成视频(只负责调用AI服务)
  • save_result:保存结果
  • deduct_balance:扣减余额
  • send_notification:发送通知

每个小函数,都只有几十行,职责清晰,容易理解,容易测试。原来的generate_video函数,变成了一个编排函数,按顺序调用这些小函数。只有几十行。

拆分之后,代码的可读性大大提高。你要改视频生成的逻辑,只需要看generatevideo这个小函数;你要改余额扣减的逻辑,只需要看deductbalance这个小函数。

第三步,规范命名,自解释。

然后,我把所有不规范的变量名、函数名、类名,都改成了有意义的名字,让代码自解释。

变量名,要能看出变量的含义和类型。比如,把a改成user,把b改成userid,把c改成balance,把d改成balanceamount,把e改成hassufficientbalance。

函数名,要能看出函数做了什么。比如,把do改成generatevideo,把handle改成processpayment,把get改成getuserby_id。

类名,要能看出类的职责。比如,把Data改成VideoGenerator,把Manager改成PaymentService。

规范命名之后,代码的可读性大大提高。很多时候,你不需要看注释,只看变量名和函数名,就能知道代码在做什么。

第四步,消除重复,提取公共逻辑。

接下来,我把到处都是的重复代码,提取成公共的函数或类,消除重复。

比如,检查用户余额的逻辑,在三个地方各写了一遍。我把它提取成一个公共函数checkuserbalance,放在UserService里,三个地方都调用这个函数。这样,要改余额检查的逻辑,只需要改一个地方。

再比如,调用AI服务的逻辑,在剧本生成、角色生成、场景生成、视频生成四个地方各写了一遍。我把它提取成一个公共的AIClient类,封装了AI服务的调用、重试、错误处理。四个地方都用这个AIClient来调用AI服务。

消除重复之后,代码量减少了很多。而且维护起来也方便了。要改一个逻辑,只需要改一个地方,不会出现改了这里忘了那里的情况。

第五步,消除硬编码,配置化。

然后,我把所有的硬编码,都改成了配置化管理。

我用pydantic-settings做配置管理,所有的配置都从环境变量读取,包括:数据库地址、Redis地址、AI服务的API地址和密钥、价格、阈值、文件存储路径等。

我还加了不同环境的配置文件,开发环境、测试环境、生产环境,各有各的配置,互不干扰。

配置化之后,要改配置,不需要改代码,只需要改环境变量或者配置文件,不需要重新部署。而且,不同环境的配置也不会混乱了。

第六步,加错误处理,健壮性。

接下来,我给代码加了完善的错误处理。

首先,我定义了一套自定义异常体系,包括:参数错误、权限不足、余额不足、资源不存在、AI服务调用失败、支付失败等。每个异常,都有对应的错误码和错误信息。

然后,在业务逻辑层,遇到错误的时候,抛出对应的自定义异常。在表现层,用全局异常处理器,捕获所有的异常,转换成友好的HTTP响应,返回给用户。

比如,用户余额不足的时候,业务层抛出BalanceInsufficientError,全局异常处理器捕获这个异常,返回400状态码,错误信息是"余额不足,请充值"。用户看到的是友好的提示,而不是500错误。

对于调用外部服务(比如AI服务、支付服务),我加了重试机制和降级处理。如果调用失败,自动重试几次;如果重试还是失败,就降级处理,返回友好的错误信息,而不是让程序崩溃。

加了错误处理之后,程序的健壮性大大提高。出了问题,不会直接崩溃,而是返回友好的提示。而且错误信息清晰,容易定位问题。

第七步,加日志,可观测。

然后,我给代码加了完善的日志。

我用Python的logging模块,建立了分级日志体系:DEBUG、INFO、WARNING、ERROR、CRITICAL。不同级别的日志,输出到不同的地方。

在关键的业务节点,我都加了日志:用户登录、创建项目、开始生成视频、生成剧本完成、生成视频完成、支付成功、支付失败等。日志里包含了关键的信息,比如用户ID、项目ID、任务ID、耗时、结果等。

对于异步任务(比如视频生成),我加了更详细的日志,记录任务的每一步:任务开始、剧本生成开始、剧本生成完成、角色生成开始、角色生成完成、视频生成开始、视频生成完成、任务结束。每一步都记录了时间和结果。

加了日志之后,出了问题,可以通过日志快速定位问题,知道任务执行到哪一步了,是哪一步出了错,错误信息是什么。不需要靠猜,靠复现。

第八步,加类型注解,更安全。

最后,我给所有的函数都加了类型注解,参数的类型、返回值的类型,都标注清楚。

类型注解的好处是:第一,代码更清晰,你一看就知道函数需要什么参数,返回什么值;第二,IDE可以做类型检查,在写代码的时候就能发现类型错误;第三,可以用mypy做静态类型检查,在运行之前就发现潜在的bug。

加了类型注解之后,代码的安全性和可维护性都提高了。

重构的效果

经过一个月的重构,这个项目的代码,发生了翻天覆地的变化。

第一,代码结构清晰了。

从原来一个5000多行的main.py,变成了分层架构、模块化的结构。每个模块都有清晰的职责,代码结构一目了然。新人接手,只需要看一下目录结构,就知道每个功能的代码在哪里。

第二,代码可读性提高了。

从原来变量名是a、b、c,函数有800行,没有注释,变成了变量名有意义,函数短小精悍,关键地方有注释。现在看代码,不需要靠猜,看变量名和函数名,就知道代码在做什么。

第三,代码可维护性提高了。

从原来改一个地方,另一个地方就出问题,变成了模块化、低耦合。改一个模块的代码,不会影响其他模块。加新功能、修bug的效率,提高了好几倍。

第四,代码健壮性提高了。

从原来没有错误处理,出了问题就500错误,变成了完善的错误处理,出了问题返回友好的提示。程序不再轻易崩溃,用户体验好了很多。

第五,代码可观测性提高了。

从原来没有日志,出了问题靠猜,变成了完善的日志体系,出了问题可以通过日志快速定位。运维和调试的效率,提高了很多。

第六,代码量减少了。

虽然加了很多新的东西(错误处理、日志、类型注解、注释),但因为消除了大量的重复代码,代码总量反而减少了。从原来的5000多行,变成了大约4000行。而且质量高了很多。

重构的经验和教训

在这次重构的过程中,我也总结了一些经验和教训:

第一,不要一上来就重写。

面对烂代码,很多人的第一反应是重写。但重写的风险很大,业务逻辑很复杂,重写很容易遗漏一些细节,引入新的bug。而且,重写需要很长时间,在这期间,旧代码还要维护,新旧切换也很麻烦。

更好的方式是渐进式重构,在不改变外部行为的前提下,逐步改善代码的内部结构。这样风险小,每次重构的范围有限,出了问题也容易回滚。

第二,重构之前,先建立测试。

重构的前提是,你要能确保你的重构没有改变原来的行为。而确保行为不变的最好方式,就是测试。

在重构之前,先给核心功能写测试。有了测试,你在重构的时候,就可以随时运行测试,确保重构是安全的。如果没有测试,你重构的时候心里就没底,不知道改坏了没有。

第三,理解业务逻辑,是重构的前提。

在重构之前,一定要花时间理解业务逻辑。只有真正理解了业务,你才能安全地重构,才能知道哪些代码是做什么的,哪些逻辑是不能改的。

如果不理解业务就重构,很容易把业务逻辑改坏,引入新的bug。

第四,从小处着手,逐步推进。

不要想着一口吃成个胖子,一次就把整个项目重构完。要从小处着手,从一个模块、一个函数开始,逐步推进。

从小处着手的好处是,风险小,每次重构的范围有限,出了问题也容易定位和回滚。而且,每完成一个小模块的重构,都会有成就感,能激励你继续下去。

第五,重构和新功能分开。

在重构期间,尽量不要加新功能。重构和新功能混在一起,很容易出问题,也很难测试。

如果必须加新功能,先把新功能加在旧代码里,等重构到这个模块的时候,再一起优化。

第六,持续提交,随时可以回滚。

重构的时候,要频繁提交代码,每完成一个小的重构,就提交一次。这样。如果出了问题,可以随时回滚到上一个正常的版本,不会丢失太多工作。

第七,不要追求完美。

重构不是一次就能完成的,也不是一次就能做到完美的。要接受不完美,先把最核心、最影响维护的问题解决了,其他的问题,可以以后慢慢优化。

追求完美,会让你陷入细节,迟迟不能完成重构,反而增加了风险。

第八,重构是持续的,不是一次性的。

代码的质量,需要持续维护。不是重构完了就万事大吉了。以后加新功能、修bug的时候,也要注意代码质量,随时重构,保持代码的优雅。

如果重构完了,又开始写烂代码,那过不了多久,代码又会变回烂代码。

如何写出优雅的代码

最后,结合这次重构的经历,聊聊如何写出优雅的代码。

第一,遵循单一职责原则。

每个函数、每个类、每个模块,只做一件事。不要把所有的逻辑都写在一个函数里,不要把所有的功能都写在一个模块里。

单一职责的好处是,代码清晰,容易理解,容易测试,容易维护。

第二,命名要规范,有意义。

变量名、函数名、类名,都要有意义,能看出它的含义和职责。不要用a、b、c、temp这种没有意义的名字。

好的命名,能让代码自解释,不需要看注释,就知道代码在做什么。

第三,函数要短小。

函数的长度,最好控制在50行以内。如果一个函数超过了50行,就应该考虑拆分了。

短小的函数,容易理解,容易测试,容易复用。

第四,消除重复。

不要复制粘贴代码。同样的逻辑,只写一遍,需要用的时候调用就好。

重复代码,是维护的噩梦。要改一个逻辑,得改好几个地方。而且很容易漏改。

第五,加注释,但不要过度。

关键的业务逻辑、复杂的算法、容易误解的地方,要加注释,解释代码的意图和原理。

但不要给每一行代码都加注释,尤其是那种# 初始化变量# 循环这种没用的注释。好的代码,本身就是最好的注释。

第六,加错误处理。

不要假设所有的操作都会成功。调用外部服务、操作数据库、处理用户输入,都要考虑失败的情况,加错误处理。

好的错误处理,能让程序更健壮,出了问题也能友好地提示用户,而不是直接崩溃。

第七,加日志。

关键的业务节点,要加日志,记录关键的信息。出了问题,可以通过日志快速定位。

但不要加太多没用的日志,比如每一行都打日志,那样日志会被淹没,真正有用的信息反而找不到了。

第八,加类型注解。

给函数的参数和返回值,加上类型注解。类型注解,能让代码更清晰,更安全,IDE也能做更好的提示和检查。

第九,遵循设计模式,但不要过度设计。

合适的设计模式,能让代码更优雅,更可扩展。比如,工厂模式、策略模式、观察者模式,在合适的场景下使用,能大大提高代码的质量。

但不要为了用设计模式而用设计模式,不要过度设计。简单的问题,用简单的方式解决就好。过度设计,会让代码变得复杂,反而不好维护。

第十,持续重构。

写代码不是一次写完就完事了。随着业务的变化,代码也需要不断地优化和重构。每次加新功能、修bug的时候,都要看看周围的代码,有没有可以优化的地方,顺手重构一下。

持续重构,能让代码始终保持优雅,不会慢慢腐烂。

写在最后

这次AI电影项目的代码重构,是我近年来做过的最有成就感的事情之一。

看着代码从一团乱麻,变成清晰、优雅、可维护的结构,那种成就感,是很难用语言来形容的。

当然,重构的过程是辛苦的。要花很多时间看代码,理解业务,要小心翼翼地改代码,确保不改变原来的行为,要反复测试,确保没有引入新的bug。

但这些辛苦,都是值得的。重构之后,代码的质量提高了,维护的效率提高了,加新功能、修bug的痛苦减少了。这些长期的收益,远远大于重构的短期投入。

如果你也在维护烂代码。如果你也每天被烂代码折磨,不要抱怨,不要放弃。行动起来,从一个小的模块开始,逐步重构。只要你坚持下去,代码一定会越来越好。

如果你是从零开始写新项目。那么从一开始就注意代码质量,遵循好的编码规范,写出优雅的代码。不要给自己留技术债务。因为技术债务迟早是要还的。而且还的时候,利息会很高。

最后,想说一句:代码是写给人看的,只是顺便能在机器上运行。写出优雅的代码,是对自己负责,也是对后来的维护者负责。

愿我们都能写出优雅的代码,远离烂代码的折磨。