去年,我们团队接手了一个AI眼镜应用的项目。这个项目已经开发了两年多,功能很完整,但代码质量非常差:一个文件几千行,一个函数几百行,变量命名混乱,注释很少,重复代码到处都是,没有单元测试,bug层出不穷。

每次加新功能,都要花很长时间理解代码;每次改一个地方,都可能引入新的bug;每次上线,都心惊胆战,怕出问题。团队的开发效率越来越低,大家都很痛苦。

于是,我们启动了一个代码重构专项,目标是把这个项目的代码,从烂代码重构成优雅、可维护、可扩展的代码。

重构花了三个月的时间,过程很痛苦,但结果很美好。重构之后,代码量减少了30%,bug率下降了60%,开发效率提升了一倍,团队的士气也高了很多。

这篇文章,我想分享一下这次AI眼镜代码重构的实战经验,从识别代码坏味道到具体的重构手段,聊聊我们是怎么把烂代码重构成优雅代码的。

如果你也在维护一个烂代码项目,或者正在考虑重构,希望我的经验能给你一些参考。

烂代码长什么样

先说说我们接手的这个项目的代码,到底有多烂。

这个项目是一个AI眼镜应用,主要功能包括:实时翻译、会议记录、视觉识别、导航、拍照录像、信息查询等。代码量大约有15万行,主要是Kotlin(Android端)和Swift(iOS端),还有一些C++(底层算法)。

代码的问题,主要有以下几个方面:

第一,文件过大。很多文件都有几千行,最大的一个文件有8000多行。一个文件里包含了十几个类,几十个函数,各种逻辑混在一起。想找一个函数,要翻半天;想改一个地方,要仔细看很久,怕影响其他逻辑。

第二,函数过长。很多函数都有几百行,最长的一个函数有500多行。一个函数里做了很多事情:初始化UI、处理用户输入、调用网络请求、解析数据、更新UI、处理错误、保存数据,全在一个函数里。这样的函数,根本无法测试,也无法复用。

第三,命名混乱。变量命名很随意:a、b、c、data、info、temp、str、list,各种没有意义的名字。有的变量用拼音,有的用英文,有的中英文混合。同一个东西,在不同的地方有不同的名字;不同的东西,有时候又用同一个名字。看代码的时候,根本不知道变量是什么意思,要反复看上下文。

第四,注释很少。代码里几乎没有注释,只有少数几个地方有注释,而且注释还是错的(代码改了,注释没改)。复杂的业务逻辑,没有任何说明,只能靠猜。新人接手,要花很长时间才能理解代码在做什么。

第五,重复代码多。同样的逻辑,在很多地方重复出现。比如,网络请求的代码,在十几个地方都有,每个地方的写法还略有不同;数据解析的代码,也是到处都是;UI更新的代码,也是重复的。想改一个逻辑,要改十几个地方,很容易漏改,导致bug。

第六,没有架构。代码没有清晰的架构,UI、业务逻辑、数据访问、网络请求,全混在一起。Activity/Fragment里什么都做:发网络请求、解析数据、操作数据库、更新UI,全在一个类里。这样的代码,无法测试,无法复用,无法维护。

第七,魔法数字和魔法字符串。代码里到处都是魔法数字和魔法字符串:if (status == 1)、if (type == "a")、timeout = 30、retryCount = 3。这些数字和字符串,没有任何说明,不知道是什么意思,也不知道为什么是这个值。想改一个值,要全局搜索,很容易漏改。

第八,没有错误处理。很多地方没有错误处理:网络请求失败了,直接崩溃;数据解析失败了,直接崩溃;用户输入不合法,直接崩溃。有的地方虽然有try-catch,但catch里什么都不做,或者只打一行log,然后继续执行,导致后续逻辑出错。

第九,没有单元测试。整个项目,没有一个单元测试。所有的测试,都是手动测试。每次改代码,都要手动测试所有相关功能,费时费力,而且很容易漏测,导致bug上线。

第十,依赖混乱。项目的依赖很混乱:同一个功能,用了好几个不同的库;有的库版本很老,有已知的bug;有的库已经停止维护了;有的库和其他库有冲突。想升级一个库,可能会导致其他库出问题。

这些问题叠加在一起,让这个项目的代码变成了一个典型的烂代码项目。维护这样的代码,是一件非常痛苦的事情。

为什么要重构

面对这样的烂代码,我们有两个选择:一是继续在烂代码上堆功能,二是停下来重构。

一开始,我们想继续堆功能,因为重构需要时间,而且有风险。但很快我们就发现,在烂代码上堆功能,效率太低了,而且bug越来越多。

具体来说,我们遇到了以下几个问题:

第一,开发效率低。加一个新功能,要花很长时间理解代码,然后在混乱的代码里找地方加,加完之后还要测试各种边界情况。一个简单的功能,可能要花一周的时间;一个复杂的功能,可能要花一个月。

第二,bug率高。因为代码混乱,逻辑不清晰,每次改代码都可能引入新的bug。而且,因为没有单元测试,很多bug要到上线之后才发现,然后又要紧急修复,恶性循环。

第三,新人上手难。新人接手这个项目,要花一两个月的时间才能理解代码,才能独立开发。而且,因为代码太烂,新人很容易写出更烂的代码,让项目越来越糟。

第四,团队士气低。每天面对烂代码,大家都很痛苦,很有挫败感。很多人都不想维护这个项目,想换项目,甚至想离职。

第五,技术债越积越多。每次为了赶进度,都在烂代码上堆更多的烂代码,技术债越积越多。到最后,可能整个项目都无法维护了,只能推倒重来。

基于这些问题,我们最终决定,停下来重构。虽然重构需要时间,有风险,但从长远来看,重构是值得的。不重构,项目只会越来越糟,最终无法维护;重构,虽然短期痛苦,但长期来看,能让项目健康发展。

重构前的准备

重构不是说改就改的,需要做好充分的准备。我们在重构之前,做了以下几件事情:

第一,争取管理层的支持。重构需要时间和人力,而且短期内看不到明显的产出,所以必须争取管理层的支持。我们向管理层说明了项目的现状、问题、风险,以及重构的必要性和预期收益。管理层最终同意了,给了我们三个月的时间,专门做重构。

第二,冻结新功能。重构期间,我们冻结了新功能的开发,只修复严重的bug。这样,我们可以专注于重构,不用一边加新功能一边重构,避免混乱。

第三,建立测试基线。重构之前,我们先对现有的功能做了全面的测试,建立了一个测试基线。我们把所有的功能点都列出来,每个功能点都有详细的测试用例。这样,重构之后,我们可以对照测试基线,确保所有功能都正常工作,没有引入回归bug。

第四,搭建CI/CD。重构之前,我们搭建了持续集成和持续部署(CI/CD)流水线。每次提交代码,都会自动编译、运行测试、检查代码规范。这样,我们可以及时发现重构中引入的问题,确保代码质量。

第五,制定重构计划。我们制定了详细的重构计划,分阶段、分模块地进行重构。不是一下子把所有代码都重写,而是一个模块一个模块地来,每个模块重构完,测试通过,再进行下一个模块。这样,风险可控,而且可以随时看到进展。

第六,制定代码规范。重构之前,我们制定了统一的代码规范:命名规范、注释规范、格式规范、架构规范、设计原则等。所有团队成员都要遵守这些规范,确保重构后的代码风格统一,质量一致。

做好这些准备之后,我们才开始正式的重构。

重构的原则

在重构的过程中,我们遵循了以下几个原则:

第一,小步快跑。重构不是一下子把所有代码都重写,而是小步快跑,每次只改一小部分,改完就测试,确保没问题。这样,风险可控,而且如果出了问题,也容易回滚。

第二,保持功能不变。重构的目标是改善代码质量,而不是改变功能。在重构的过程中,我们尽量保持功能不变,只改代码的结构和实现方式。这样,测试的时候,只需要对照原来的功能,确保行为一致就行。

第三,先测试,后重构。在重构一个模块之前,我们先给这个模块写单元测试,确保原来的功能是正确的。然后,再进行重构,重构完之后,运行单元测试,确保功能没有变化。这是重构的安全网。

第四,持续集成。每次重构完一小部分,就提交代码,触发CI/CD,自动编译、测试、检查。这样,可以及时发现问题,确保代码质量。

第五,团队协作。重构不是一个人的事情,而是整个团队的事情。我们定期开会,讨论重构的进展、遇到的问题、解决方案。大家互相review代码,确保重构的质量。

第六,不追求完美。重构不是要把代码改得完美无缺,而是要把代码改得比原来好,更易维护、更易扩展。不要为了追求完美而过度设计,过度重构。够用就好,以后有需要再继续优化。

具体的重构手段

说了这么多,现在说说我们具体用了哪些重构手段。

第一,拆分大文件和大函数。这是我们做的第一件事情,也是效果最明显的一件事情。

我们把几千行的大文件,按照功能拆分成多个小文件,每个文件只负责一个功能。比如,原来的MainActivity有8000多行,我们拆分成了:MainActivity(只负责UI和事件处理)、MainViewModel(负责业务逻辑)、MainRepository(负责数据访问)、MainApiService(负责网络请求)、MainUiState(负责UI状态)等。

我们把几百行的大函数,拆分成多个小函数,每个函数只做一件事情。比如,原来的onCreate函数有500多行,我们拆分成了:initView()、initData()、initObserver()、initListener()等小函数,每个函数只做一件事情,职责清晰。

拆分之后,代码的可读性大大提升,找一个函数不用翻半天了,改一个地方也不用担心影响其他逻辑了。

第二,统一命名。我们对所有的变量、函数、类、文件,都按照统一的命名规范重新命名。

变量命名,要能表达变量的含义,不用a、b、c、data、info这种没有意义的名字。比如,把data改成userInfo,把list改成translationResults,把temp改成cachedResponse。

函数命名,要能表达函数的行为,用动词开头。比如,把getData()改成fetchTranslationResult(),把update()改成updateUiWithResult(),把save()改成saveToDatabase()。

类命名,要能表达类的职责,用名词。比如,把Manager改成TranslationManager,把Helper改成NavigationHelper,把Util改成DateUtil。

统一命名之后,代码的可读性大大提升,看名字就知道变量、函数、类是做什么的,不用反复看上下文了。

第三,补充注释。我们在关键的地方补充了注释:

  • 每个类,都有类注释,说明这个类的职责。
  • 每个公开的函数,都有函数注释,说明函数的功能、参数、返回值。
  • 复杂的业务逻辑,有详细的注释,说明为什么这么做,边界情况是什么。
  • 魔法数字和魔法字符串,都定义成常量,并有注释说明含义。

补充注释之后,新人接手代码,理解起来快了很多,不用靠猜了。

第四,消除重复代码。我们把重复的代码,抽取成公共的函数、类、组件。

比如,网络请求的代码,原来在十几个地方都有,我们抽取成了一个统一的网络请求框架:ApiClient(负责网络请求的配置和发送)、BaseResponse(统一的响应格式)、Result(统一的结果封装)、CoroutineHelper(协程辅助函数)。所有的网络请求,都通过这个统一的框架来做,不再到处写重复的代码。

比如,数据解析的代码,原来到处都是,我们抽取成了统一的解析函数,用Gson/Moshi的适配器来做,统一处理解析错误。

比如,UI更新的代码,原来到处都是,我们抽取成了统一的UI组件和扩展函数,比如View的扩展函数、Activity的扩展函数、Fragment的扩展函数。

消除重复代码之后,代码量减少了30%,而且想改一个逻辑,只需要改一个地方,不用改十几个地方了,bug率也下降了。

第五,引入清晰的架构。我们引入了MVVM架构,把UI、业务逻辑、数据访问分离开。

  • View层(Activity/Fragment):只负责UI的展示和用户事件的处理,不做业务逻辑,不做数据访问。
  • ViewModel层:负责业务逻辑,处理用户事件,调用Repository获取数据,更新UI状态。
  • Repository层:负责数据访问,从网络、数据库、缓存中获取数据,统一处理数据来源。
  • DataSource层:负责具体的数据获取,包括ApiService(网络请求)、Dao(数据库访问)、Cache(缓存)。

引入清晰的架构之后,代码的职责清晰了,每个层只做自己的事情,不再混在一起。这样的代码,易测试、易复用、易维护。

第六,消除魔法数字和魔法字符串。我们把所有的魔法数字和魔法字符串,都定义成常量。

比如,把if (status == 1)改成if (status == TranslationStatus.SUCCESS),把timeout = 30改成const val NETWORKTIMEOUT = 30000L,把type == "a"改成type == ItemType.TRANSLATION。

常量都定义在统一的常量类里,或者对应的枚举类里,并有注释说明含义。这样,想改一个值,只需要改一个地方,而且看名字就知道是什么意思,不用猜了。

第七,完善错误处理。我们对所有可能出错的地方,都完善了错误处理。

  • 网络请求:统一处理网络错误、超时错误、服务器错误,根据错误类型给用户不同的提示。
  • 数据解析:统一处理解析错误,不会因为一条数据解析失败而导致整个列表崩溃。
  • 用户输入:对用户的输入做合法性校验,不合法的输入给用户提示,不会导致崩溃。
  • 异步操作:对协程、线程中的异常,都做了捕获和处理,不会因为一个异步操作失败而导致应用崩溃。

完善错误处理之后,应用的崩溃率大大下降,用户体验好了很多。

第八,补充单元测试。我们给核心的业务逻辑,都补充了单元测试。

  • ViewModel的测试:测试业务逻辑是否正确,用户事件处理是否正确,UI状态更新是否正确。
  • Repository的测试:测试数据获取是否正确,数据缓存是否正确,错误处理是否正确。
  • 工具类的测试:测试各种工具函数是否正确,边界情况是否处理正确。
  • 数据模型的测试:测试数据解析是否正确,数据转换是否正确。

补充单元测试之后,我们改代码的时候,就有了安全网。改完代码,运行单元测试,如果测试通过,说明功能没有变化;如果测试失败,说明引入了bug,需要修复。这样,bug率大大下降。

第九,清理依赖。我们对项目的依赖进行了清理:

  • 移除了不再使用的依赖。
  • 升级了有bug的、老旧的依赖到最新版本。
  • 同一个功能,只保留一个库,移除了重复的库。
  • 替换了停止维护的库,用活跃维护的库替代。
  • 解决了库之间的冲突。

清理依赖之后,项目的编译速度快了很多,包体积也小了很多,而且因为库的bug导致的问题也少了很多。

第十,代码review。我们建立了严格的代码review制度,所有的代码提交,都要经过至少一个人的review,才能合并。review的时候,重点看:命名是否规范、注释是否充分、逻辑是否清晰、是否有重复代码、是否有潜在的bug、是否符合架构规范、是否有单元测试。

代码review制度,不仅保证了重构的质量,也让团队成员互相学习,共同进步。

重构的成果

经过三个月的重构,我们取得了显著的成果:

第一,代码质量大幅提升。代码从原来的烂代码,变成了优雅、可维护、可扩展的代码。文件大小、函数长度都在合理范围内,命名规范,注释充分,没有重复代码,架构清晰,错误处理完善。

第二,代码量减少了30%。通过消除重复代码、抽取公共函数、简化逻辑,代码量从原来的15万行,减少到了10万行左右。代码更少了,但功能更完整了,逻辑更清晰了。

第三,bug率下降了60%。通过完善错误处理、补充单元测试、代码review,bug率大大下降。上线后的bug,从原来的每周十几个,减少到了每周两三个。而且,因为有单元测试,很多bug在开发阶段就发现了,不会流到线上。

第四,开发效率提升了一倍。重构之后,代码清晰了,架构合理了,加新功能的速度大大提升。原来一个简单的功能要花一周,现在两三天就能完成;原来一个复杂的功能要花一个月,现在两周就能完成。而且,因为bug少了,修复bug的时间也少了,大家可以把更多的时间用在开发新功能上。

第五,新人上手快了。原来新人要花一两个月才能理解代码,现在只要一两周就能上手,独立开发。因为代码清晰了,注释充分了,架构合理了,新人理解起来快了很多。

第六,团队士气高了。每天面对优雅的代码,大家都很开心,很有成就感。不再像以前那样,每天面对烂代码,痛苦不堪。团队的凝聚力和士气都高了很多。

第七,技术债减少了。通过重构,我们偿还了大部分的技术债,项目变得健康了。以后加新功能,不用再在烂代码上堆烂代码了,可以在良好的基础上,持续健康地发展。

重构的经验和教训

这次重构,给了我们很多经验和教训:

第一,重构要趁早。技术债越积越多,越晚重构,代价越大。不要等到项目无法维护了才想到重构,那时候可能只能推倒重来了。发现代码有问题,就要及时重构,小步快跑,持续改善。

第二,重构需要管理层的支持。重构需要时间和人力,而且短期内看不到明显的产出,所以必须争取管理层的支持。要向管理层说明重构的必要性和预期收益,让他们理解和支持重构。

第三,重构要有计划。重构不是盲目地改代码,要有详细的计划,分阶段、分模块地进行。每次只改一小部分,改完就测试,确保没问题。这样,风险可控,而且可以随时看到进展。

第四,重构要有测试保障。重构之前,要建立测试基线;重构之中,要补充单元测试;重构之后,要进行全面的回归测试。测试是重构的安全网,没有测试的重构,就是赌博。

第五,重构要保持功能不变。重构的目标是改善代码质量,而不是改变功能。在重构的过程中,要尽量保持功能不变,只改代码的结构和实现方式。这样,测试的时候,只需要对照原来的功能,确保行为一致就行。

第六,重构要团队协作。重构不是一个人的事情,而是整个团队的事情。要让所有团队成员都参与进来,共同制定代码规范,共同review代码,共同推进重构。只有团队所有人都认同和参与,重构才能成功。

第七,重构不要追求完美。重构不是要把代码改得完美无缺,而是要把代码改得比原来好。不要为了追求完美而过度设计,过度重构。够用就好,以后有需要再继续优化。代码是持续演进的,不是一次重构就能完美的。

第八,重构之后要保持。重构完成之后,要建立制度,保持代码质量。比如,代码review制度、单元测试制度、代码规范检查制度。如果重构之后,又开始写烂代码,那很快就会回到原来的样子。重构只是开始,保持代码质量才是长期的事情。

写在最后

这次AI眼镜代码重构,是我们团队经历过的最痛苦、但也最有价值的一件事情。

重构的过程很痛苦:要理解烂代码,要拆大函数,要改命名,要补注释,要消除重复代码,要引入架构,要补单元测试,要做回归测试。每天都在和烂代码作斗争,有时候改了一天,发现还有更多的烂代码,很有挫败感。

但重构的结果很美好:代码变优雅了,bug变少了,效率提升了,团队士气高了。大家都很有成就感,觉得这三个月的努力是值得的。

如果你也在维护一个烂代码项目,我强烈建议你,找个时间,停下来重构。虽然短期痛苦,但长期来看,重构是值得的。不重构,项目只会越来越糟,最终无法维护;重构,虽然短期痛苦,但长期来看,能让项目健康发展。

当然,重构不是一件容易的事情,需要做好充分的准备,需要管理层的支持,需要团队的协作,需要详细的计划,需要测试的保障。但只要你认真去做,就一定能成功。

最后,想对所有的开发者说一句:写代码的时候,多想想以后维护你代码的人。那个人,可能就是半年后的你自己。写优雅的代码,不仅是对项目负责,也是对自己负责。

愿我们都能写出优雅、可维护、可扩展的代码,不再被烂代码折磨。

重构之路,道阻且长,行则将至。