点击上方蓝字关注我们
您是否曾经在开始新工作时,面对复杂的代码库不知从何下手?你并不孤单。我们中的许多人都曾有过这样的经历--试图理清仍在运行业务主要部分的过时代码。2024 年 Stack Overflow 的一项调查发现,超过 80% 的开发人员经常与遗留代码打交道,因此这是我们这个行业面临的共同挑战。大多数遗留软件都有其价值,因为它们仍在使用并拥有客户。在文章的其余部分,我们将讨论您可能面临的典型问题,如过时的技术、缺失的文档和堆积如山的技术债务。更重要的是,我们将分享一些实用步骤,帮助你理解和驾驭这些旧系统。探讨包括 GitHub Copilot 这样的人工智能助手在内的现代工具如何让生活变得更轻松。那么,让我们开始吧。
浏览遗留代码库
假设你刚加入一个新团队,面对的是一个庞大的代码库,其存在时间比你的大多数同事都要长。文档很旧,最后一次更新是在 2009 年。最初的开发人员早已不在人世,但这个系统却为关键业务运营提供了动力。如何解决这个问题?根据 Stack Overflow 在 2024 年进行的一项调查,超过 80% 的开发人员经常与遗留代码打交道,这也是我们这个行业面临的普遍挑战。我们中的大多数人都有接触过遗留代码库的经历,或者你目前正处于这种情况下。这种经历的强度因不同的因素而异,例如代码库的范围有多广、代码有多好、是否有测试、公司里是否有人可以询问等。遗留代码有不同的定义。有人说它是由团队或开发人员继承的代码,也有人说它是没有测试的代码,甚至还有人说 “代码一写出来就是遗留代码”。它通常是指仍在使用,但可能需要当前开发团队充分理解、充分记录或维护的代码。
处理遗留代码时面临的挑战
遗留代码通常会面临不同的挑战,例如
1.过时的技术或编程语言
遗留代码可能是使用过时的技术(不再受支持)、不遵循现代实践或不再受广泛支持或维护的编程语言编写的。
2.没有文档
在某些情况下,遗留代码可能没有足够的文档或根本没有文档,这使得开发人员更难理解其工作原理或如何在不引入新问题的情况下对其进行修改。
3.大量技术债务
遗留代码通常会积累技术债务,即维护或重构代码所需的额外工作成本,从而使代码从长远来看更容易维护。技术债务的例子包括过时的库、复杂的逻辑、死代码等。
4.集成问题
将遗留代码与较新的系统或技术相结合可能会出现问题,因为它可能依赖于过时的应用程序接口、库或数据格式。
5.测试挑战
传统代码可能需要更充分的测试,因此很难确保修改或集成不会引入新的错误或回归。
然而,在这种情况下,我们通常会感到不知所措,甚至不知从何下手。第一步是先了解代码,然后再采取行动。
理解遗留代码的推荐方法
以下是您可以采用的方法:
1. 阅读技术文档
收集现有文档,包括架构图、代码注释和用户手册。与具有代码库经验的团队成员交谈,以便更好地了解其历史、开发选择、编程语言和已知问题。尝试了解项目的业务方面以及这些代码应该做什么。
推荐工具:Confluence、用于架构图的 Draw.io、用于团队文档的 Notion
2. 识别和分析热点
关注热点,即代码中经常更改的部分,因为这很可能就是你要工作的地方。与用户和同事交谈。尝试找出业务需求并写下所有内容。要求你的经理或团队领导分配给你一个错误修复或小功能,让你尽早接触代码库,或者与你的队友一起开展任何活动,如拉取请求审查,让你主动学习代码库,而不是被动地逐行阅读。
有用的工具:Git blame、用于热点分析的 CodeScene、用于代码质量度量的 SonarQube
3. 建立开发环境
建立一个与生产设置非常相似的本地开发环境。确保可以在本地构建和运行应用程序,并访问必要的数据库、服务和配置文件。在推向生产环境之前,请务必尝试在开发/测试环境中所做的工作。
有用的工具:用于容器化的 Docker、用于日志记录的 ELK Stack、用于监控的 New Relic 或 Datadog
4. 确保拥有所有必要的工具
检查版本控制系统,查看所接触文件的历史更改记录。如果没有,第一步就是建立它。我们需要它来了解过去的更改及其背后的原因。接下来,确保你有一些持续集成和错误跟踪系统。
推荐堆栈:Git、Jenkins/GitHub Actions、Azure DevOps
5. 关注入口点和关键功能
确定应用程序的入口点,如主方法或初始化脚本。跟踪这些入口点的执行路径,以了解应用程序的流程。使用集成开发环境、调试器和日志框架等调试工具来跟踪问题并更好地理解代码库。
调试方法:使用日志框架、逐步调试和性能剖析工具
6. 识别依赖项和模块
分析代码库,识别依赖项、库和框架。了解系统中每个模块或软件包的目的和功能。熟悉主要组件及其交互,创建系统架构的高层地图。
有用的工具:Structure101、Java 项目的 JDepend、JavaScript 的 npm-dependency-graph
7. 使用功能标志
使用功能标志feature flags的目的是为了在出错时关闭代码(即回滚策略),并使您能在想启动时启动代码。有了特性标志,你就不必等待下一次(正式)发布。不过,要注意清理它们,因为它们会带来新的复杂性(例如,创建一个文档来跟踪它们)。
场景描述:在处理遗留代码时,重构和迁移是常见的任务。使用Feature Flags,可以在不中断服务的情况下,逐步将旧代码替换为新代码。
意义:这种方式确保了系统的平滑过渡,降低了重构和迁移过程中的风险。同时,通过逐步替换和验证,可以确保新代码的正确性和稳定性。
8. 谨慎编写测试和重构
遗留代码库通常缺乏足够的测试覆盖率。不要碰任何未经测试的东西!在进行任何修改之前,编写单元测试和集成测试,确保修改不会破坏现有功能。重构时,应从小规模的增量更改开始,避免可能会引入新问题的大规模重写。这种方法被称为 “Strangler Fig”。
Strangler Fig(榕树模式或绞杀者模式)是一种设计模式,由Martin Fowler在2004年首次提出(2019年改名为Strangler Fig Application)。Strangler Fig模式描述了一种逐步而非一次性进行系统迁移的优雅方法。在软件环境中,它涉及在旧系统的边界周围构建一个新系统,并允许逐步用新系统的组件替换旧系统的部分。随着旧系统的功能被新系统逐步取代,最终新系统将完全取代旧系统的所有功能,使旧系统停用。
构建外层:创建一个外层来拦截请求前往后端旧版系统。外层可将这些请求路由到旧版应用程序或新服务。
逐步迁移:将旧系统的功能逐步迁移到新系统。这可以通过添加新功能到新系统,并相应地更新外层路由来实现。
同步更新:确保外层与迁移保持同步,以便正确路由请求。同时,需要确保外层不会成为单一故障点或性能瓶颈。
停用旧系统:当所有功能都已迁移到新系统后,可以安全停用旧版系统。
如果发现方法过长,应将其分解为新的、更短的方法。你还应该清理死代码,删除魔法数字,并应用其他清洁代码原则。测试方法建议:
1)为现有行为编写特性测试
通过测试来明确和验证软件当前的行为特征,以确保在后续的开发和维护过程中不会意外地改变这些行为。特征测试是一种测试类型,它关注于软件系统的特定行为或功能,并验证这些行为或功能是否按照预期工作。与单元测试不同,特征测试通常更加宏观,涉及多个模块或组件的交互,并关注于系统的整体行为。特征测试的目的是确保软件系统的关键行为在不同场景下都能保持一致和正确。
识别关键行为:首先,需要识别软件系统中的关键行为,这些行为可能是用户经常使用的功能,或者是系统的核心业务流程。
编写测试用例:针对每个关键行为,编写相应的测试用例。测试用例应该覆盖该行为的所有重要场景和边界情况。
使用自动化测试工具:尽可能使用自动化测试工具来编写和运行特征测试。这可以提高测试效率和准确性,并减少人为错误的可能性。
验证测试结果:运行测试用例并验证结果是否符合预期。如果测试失败,则需要检查代码并修复问题,直到测试通过为止。
2)为计划更改的区域添加单元测试
单元测试是软件测试中最基础的测试类型之一,它针对软件中的最小可测试单元(通常是函数或模块)进行测试。单元测试的主要目的是验证代码的行为是否符合预期,确保代码的正确性和稳定性。通过单元测试,可以在早期阶段发现潜在的问题和错误,从而降低软件发布后的风险。
识别需要测试的区域:首先,需要明确计划修改的代码区域,并识别出哪些部分需要进行单元测试。
编写测试用例:针对需要测试的区域,编写相应的测试用例。测试用例应该覆盖该区域的所有重要功能和边界情况。
运行测试并验证结果:在修改代码之前,运行测试用例并验证其结果是否符合预期。如果测试失败,则需要修复代码直到测试通过。
在修改后重新运行测试:在修改代码后,重新运行测试用例以确保修改没有引入新的错误。如果测试失败,则需要检查修改并修复问题。
3)对关键路径实施集成测试
关键路径是指在软件测试过程中,从开始至结束的最长时间路径。它代表了测试过程中的瓶颈,决定了整体测试的时长和进度。关键路径上的测试任务通常具有较高的优先级和重要性,一旦这些任务延迟或出现问题,将对整个测试计划产生重大影响。
集成测试是在单元测试的基础上,将所有模块按照设计要求组装成系统进行测试的过程。它的主要目的是检查模块之间接口的正确性、模块之间数据的传递以及系统功能的完整性。通过集成测试,可以确保软件在关键路径上的各个模块能够正确协同工作,从而实现整体功能。
4)使用突变测试验证测试质量
突变测试的核心思想是对程序的源代码或目标代码进行小的改动,并把这种改动产生的截然不同的错误行为(或怪异行为)作为预期。如果测试代码没有觉察到这种小改动带来的错误,就说明这个测试是有问题的。通过这种方式,突变测试可以帮助找到那些可能被忽略的错误和弱点。
衡量测试质量:通过改变源代码并观察测试代码的反应,可以评估测试的完整性和有效性。如果测试能够成功检测到这些小的改动(即变异),则说明测试具有较高的质量。
定位代码弱点:突变测试还可以帮助开发者定位代码中的弱点和冗余部分,从而提高代码的质量和效率。
9. 记录你的发现
在探索代码库的过程中,记录下您的发现和见解。这些文档将帮助您和您的团队更好地理解系统,并更有效地进行未来的更改。此外,如果有人要求你彻底重写应用程序,请说 “不”!尤其是在规模较大的情况下。即使有一些合理的理由,这通常也是一项重大且不可预测的工作。
推荐工具: 架构决策记录(ADR)、用于 API 文档的 Swagger、软件源中的 README.md 文件。
概念:ADR,即Architectural Decision Records,意味着记录架构决策的一种实践。它涉及通过持续记录软件架构决策过程中重要的信息,以便未来的开发者和决策者可以理解为何当时会作出这样的选择。
目的:ADR的主要目的是提供一种机制,让团队成员能够记录下为何一个特定的技术、模式或者架构被选择,并为未来的项目参与者留下足够的上下文信息。
一个典型的ADR文档通常包括以下几个重要部分:
标题:简洁明了地描述决策的主题。
背景:提供决策的背景信息,包括问题的提出、相关的需求和约束条件等。
决策描述:详细阐述所作出的决策,包括选择的技术、模式或架构等。
状态:描述决策的当前状态,如已实施、待实施、已废弃等。
后果:分析决策可能带来的后果,包括正面影响和潜在风险。
Architectural Decision Records(ADR)在IT行业中具有重要的作用,它能够帮助团队记录和管理架构决策,提升决策透明度和项目一致性,促进团队协作和项目维护。在实施过程中,应关注文档的清晰简洁性、及时更新性以及团队文化的推广等方面。
10. 定期进行代码审查
代码审查可以通过对拉取请求的注释、面对面聊天或远程视频聊天来完成。你可以与工程师同事、工程经理或任何熟悉代码库的人一起进行。在此了解如何正确进行代码审查。
专注于一件事
记住一次只专注于一件事。为了防止在堆积如山的代码中不知所措和迷失方向,我建议首先只专注于代码库的一个部分,理解它,然后再转向下一个部分。
更多请阅读
《修改代码的艺术Working Effectively with Legacy Code》 作者 Michael Feathers
《重构:改进现有代码的设计》,Martin Fowler 著
《整洁代码》 “罗伯特-马丁(Robert C. Martin)著
使用人工智能AI工具浏览遗留代码库
如今,你可以使用不同的人工智能工具来帮助你浏览遗留代码库,例如 GitHub Copilot。它能从注释和代码中理解上下文,帮助你更快地编写代码,减少错误。在处理遗留代码时,Copilot 可以
1)提供符合现有模式的代码补全。
2)为不熟悉的代码片段提供解释建议。
3)为文档不完善的代码生成测试和文档。
如何使用?首先,你需要为不同的集成开发环境安装它:
Visual Studio Code:从 VS Code Marketplace 安装。
JetBrains IDE:使用 JetBrains Marketplace 中的插件。
Neovim:按照 GitHub Copilot Neovim 代码库上的说明进行操作。
然后,你可以使用以下策略来浏览代码。
1. 绘制地图
使用人工智能来理解不熟悉的代码。以下是一些可以尝试的示例:
2. 为代码编写单元测试
在深入研究之前,让人工智能帮你创建安全网:
3. 安全地重构代码
我们可以要求 Copilot 重构或更新代码段。您可能会遇到难以阅读的复杂条件逻辑,比如下面这样的情况:
您可以请求 Copilot 使用 C# 中的现代开关表达式进行重构。
4. 生成文档
要求人工智能记录更改,这可以在代码之外完成。这可能是人工智能工具最强大的功能之一,因为它能使代码和文档保持同步。
人工智能方法的缺点
在缺点方面,我们在向人工智能提问时应小心谨慎,因为它们可能会因 LLM 模型中的幻觉而产生不好的结果,然后你需要再次提问,但在某种程度上,它们可以帮助你了解该往哪里走。正如最新的《ThogughtWorks 技术雷达》第 31 卷所指出的,有一种误解认为人类可以完全取代以人工智能为伙伴的结对编程,这导致了对编码辅助思想的过度依赖、生成代码的代码质量问题以及更快的代码库增长速度。此外,目前在 GitHub Copilot 等人工智能工具中缺少的是对代码库更广泛的理解,以及在提出修改建议时对业务逻辑和依赖关系的考虑。当这些工具更好地理解代码库、文档等内容时,它们将产生惊人的结果。这些结果将帮助我们将关注点从单纯的编码转向解决问题。
因此,当我们想使用 LLM 完成编码任务时,我们应该问问自己:我想解决什么问题?我想解决什么问题,人工智能是最佳解决方案吗?如果答案是肯定的,那就使用它。此外,考虑到这些工具每天都在不断改进,偶尔也要检查一下。总之,像 GitHub Copilot 这样的人工智能工具可以通过提供即时见解、建议和改进来帮助你处理遗留代码。普林斯顿大学、麻省理工学院和微软的研究表明,虽然 Copilot 并不是万能的,但将其集成到工作流程中可以将工作效率提高 26%。
总结
遗留代码通常指的是那些历史悠久、可能由不同团队在不同时期编写的代码,它们往往缺乏统一的架构、文档和测试覆盖。这些代码可能因为技术债务、维护困难、性能瓶颈等问题而成为项目发展的阻碍。因此,探讨处理遗留代码的方法对于提升软件质量、降低维护成本、加速新功能开发等方面都具有重要意义。
意义一:提升软件质量
遗留代码往往存在大量的技术债务,如未解决的bug、冗余代码、缺乏文档和测试等。通过处理遗留代码,可以识别和修复这些问题,从而提升软件的整体质量。高质量的代码不仅减少了出错的可能性,还提高了系统的稳定性和可靠性。
意义二:降低维护成本
遗留代码通常难以理解和维护,因为它们可能缺乏清晰的文档和注释,或者使用了过时的技术和工具。通过重构、模块化、添加文档和测试等手段处理遗留代码,可以使代码更加易于理解和维护。这降低了维护成本,提高了开发效率,并使得团队能够更快地响应市场需求和变化。
意义三:加速新功能开发
遗留代码可能包含大量的技术障碍,这些障碍会阻碍新功能的开发和部署。通过处理遗留代码,可以消除这些障碍,为新功能开发创造更加顺畅的环境。这有助于加速新功能的开发和部署,提高产品的竞争力。
意义四:促进技术演进
随着技术的不断发展,新的工具、框架和编程语言不断涌现。处理遗留代码可以使得系统更加容易采用新技术,从而保持系统的先进性和竞争力。通过逐步替换过时的技术和工具,可以使得系统更加灵活、可扩展和易于维护。
意义五:提高团队协作效率
遗留代码往往存在多个版本和分支,这使得团队协作变得复杂和困难。通过处理遗留代码,可以整合这些版本和分支,使得团队协作更加顺畅和高效。这有助于提高开发团队的凝聚力和协作效率,促进项目的顺利推进。
探讨处理遗留代码的方法在软件工程中具有重要意义。它不仅有助于提升软件质量、降低维护成本、加速新功能开发等方面,还有助于促进技术演进和提高团队协作效率。因此,在软件工程中,我们应该重视并妥善处理遗留代码问题。