继承了 .NET 噩梦?驯服遗留代码的 5 种策略
Source: Dev.to

介绍
作为开发者,几乎不可避免地会在维护或现代化已有代码库时投入工作。很少有这种情况是直接明了的;当核心逻辑纠结成一团乱麻——典型的大泥球(big ball of mud)时,挑战会让人感到不知所措。这种缺乏清晰度会削弱你的信心,并在每次更改时增加系统在意外位置崩溃的风险。
什么是遗留 .NET 代码?
人们常误以为遗留代码一定很老。虽然代码的年龄可能是一个因素,但一个系统即使使用了现代框架,也可能因为赶期限而组织混乱、临时拼凑而成为遗留代码。虽然定义遗留代码的因素有很多,今天我们重点关注其中两个最关键的方面。
- 没有测试 – 如果代码 没有测试,它就是遗留代码。Michael Feathers1 著名地将遗留代码定义为“没有测试的代码”,因为缺乏安全网会使得对系统的修改变得风险高且成本大。
- 没有自动化部署 – 如果代码和基础设施 没有自动化部署,它就是遗留代码。现代软件在云基础设施上运行时,应该能够通过自动化轻松部署。如果一次发布需要开发者在工作站上进行繁琐的手动操作,那么重新部署该应用的风险就会过高。
我应该重建我的 .NET 应用程序吗?
在接手一个庞大的 .NET 单体系统时,直接废弃它并从零开始似乎非常诱人。这种想法很诱惑,但“大爆炸”式的重写几乎总是一个陷阱。
这些大规模的重写往往会超出预期时间,因为开发人员常常仅依据对界面或 API 的表面分析来制定时间表。这种做法经常会遗漏埋藏在代码深处的未记录业务规则——这些规则有时要等到生产环境出现 bug 才会被发现。此外,在重写完成之前,新功能的开发会陷入停滞;花费两年时间采用新技术却没有任何创新,可能会让企业在财务和竞争力上受到损失。

我该如何现代化我的 .NET 遗留应用程序?
如果排除大幅重写,我们该如何前进?首先,采用一种特定的思维方式:保持谨慎和深思熟虑。
面对庞大、错综复杂的代码库,急于行动可能会在系统的其他部分引入未知的错误。你必须小心行事。例如,你可能会在方法开头看到一个看似毫无用途的 guard clause 并将其删除,却在下一个版本中出现问题。这恰好说明了 Chesterton’s fence:仅仅因为某段遗留代码对你而言没有明显的用途,并不意味着它不重要。
Source:
我可以使用哪些策略来现代化我的 .NET 代码库?
一旦拥有正确的思维方式,就可以开始采用战术性的解决方案。以下是五种经验证的策略,帮助你重新掌控 .NET 应用程序。
1. 特征化测试 – 构建安全网
Michael Feathers 最关键的策略之一是锁定系统当前的行为,即使它有缺陷。特征化测试(characterisation tests)断言系统现在实际做了什么,而不是它应该做什么。目标不是立刻修复逻辑,而是创建一个可测试的基准。拥有了这张能够证明系统行为的安全网后,你就可以自信地重构混乱的代码,而不会丢失未记录的业务规则。

2. Sprout 与 Wrap
当严格的时间限制阻止你重构周边的混乱代码时,Feathers 建议使用两种技术安全地添加功能:
- Sprout – 将新功能写成一个全新的、已完整测试的方法或类,然后在旧代码中直接调用它。
- Wrap – 给旧方法改名,创建一个同名的新方法,让新方法同时执行旧逻辑和你的新增内容。
3. 童子军规则
由 Robert C. Martin(“Uncle Bob”)推广的 童子军规则 很简单:“永远把代码留下比你发现时更干净的状态。” 当你更新一个方法时,花点时间重命名模糊的变量以提供明确含义,或删除未使用的参数。这些微小的改进会随时间累积,显著提升可维护性。同样重要的是将此规则应用到测试套件上;清理测试可以提升你对已构建安全网的信心。
4. 使用功能标记进行增量重构
将风险较大的改动包装在功能标记(feature flag)后面。将新实现与旧实现一起部署,然后在确认新代码可用后逐步将流量切换过去。这种做法可以让你:
- 在生产环境中验证行为,而无需全量发布。
- 若出现问题,只需切换标记即可立即回滚。
- 在整个重构过程中保持代码库始终可部署。
5. 采用自动化 CI/CD 流水线
即使旧系统缺乏自动化部署,也可以从小处着手:
- 创建 CI 流水线,在每次推送时运行新添加的特征化测试。
- 添加构建步骤,打包应用程序(例如使用
dotnet publish)。 - 引入部署步骤,使用 GitHub Actions、Azure Pipelines 或 GitLab CI 将代码部署到预发布环境。
自动化这些步骤能够提供快速反馈,降低人为错误,并为未来的增量改进铺平道路。
结束语
现代化一个遗留的 .NET 单体系统很少是一个快速、一锤子买卖的项目。通过构建带有特征化测试的安全网,应用 Sprout/Wrap 技术,遵循童子军规则,利用功能标志,并逐步引入 CI/CD,你可以在不冒昂贵的“全盘”重写风险的情况下,驯服即使是最纠结的代码库。
4. 绞杀树模式
If your goal is to modernise a monolith into a modular architecture, the safest route is the Strangler Fig Pattern – a concept championed by Martin Fowler ↗(Strangler Fig Application).
Imagine you have a massive, ten‑year‑old ASP.NET Web Forms app, and your operations team desperately needs a new mobile‑friendly “Driver Dispatch” module.
Instead of rewriting the whole logistics system, you place a routing proxy (a “Traffic Cop”) in front of the legacy app, initially routing 100 % of traffic to the old system. Then you build the new “Driver Dispatch” feature inside a cleanly architected and testable .NET application.
If your internal team is bogged down maintaining the old monolith, this is often a great time to leverage expert .NET application development services to ensure the new architecture is built right from day one.
Finally, you update the proxy rules so that billing clicks go to the old system, but dispatch clicks route seamlessly to the new app. Users utilise both systems simultaneously without noticing, allowing you to slowly extract modules over time.
5. 自动化 “魔法” 部署
如果你的系统需要开发人员手动执行 SQL 脚本、复制 .dll 文件,或在 Visual Studio 中右键点击 “Publish”,那么你无法安全地对其进行现代化。重构需要频繁、低风险的部署,而手动部署则是少见且高风险的。
在动摇核心架构之前,使用 GitHub Actions 或 Azure DevOps 等工具,将这些 “魔法” 步骤编入 CI/CD 流水线。知道你可以通过一次点击可靠地编译、测试和部署,能大幅减轻救援任务中的压力。
结论
拯救遗留软件并不是编写最巧妙的 C# 代码,而是关于 风险管理。这五种策略仅仅是对遗留 .NET 系统进行受控、渐进式更改以维护或现代化的起点。
如果你接手了一个复杂的遗留应用,不要气馁。通过构建测试安全网、进行小幅受控的更改、确保代码可测试以及自动化部署,你可以把令人畏惧的遗留系统转变为可管理的系统。只要有耐心、细致以及正确的工作流,任何代码库都能被控制住,以满足业务需求。
1: Feathers, Michael C. (2005). Working Effectively with Legacy Code
Footnotes
-
Michael Feathers, Working Effectively with Legacy Code (Prentice Hall, 2004). ↩


