意大利面条式代码(spaghetti code,面条)——这是程序混乱、杂乱的结构,逻辑块毫无顺序地交织在一起。根据 TIOBE Index(2024)的研究,具有高意大利面条式代码水平的项目在引入新功能时需要多 2.5 倍的时间。这个术语诞生于早期编程时代,当时 goto 语句允许程序跳转到任何位置,从而产生难以阅读的结构。
要点
意大利面条式代码(spaghetti code)——这是用来描述代码结构的比喻,其结构就像一盘意大利面:单独的线(逻辑块)缠绕、粘连在一起,彼此无法分离。在这种代码中不可能划分出层、模块或组件——一切都混在一大团中。
与糟糕代码不同,糟糕代码可能只是不够整洁,而意大利面条式代码是根本性的架构问题。即使格式完美、变量命名良好的代码,如果其架构混乱,也可能是意大利面条式代码。问题出在程序结构层面,而不是书写风格层面。
根据IEEE(2022)的数据,大型项目中约 35% 的错误正是由混乱的代码结构引起的,而不是开发者的逻辑错误。开发者出错不是因为理解错了任务,而是因为在意大利面条式代码中无法追踪执行流程。
如果糟糕代码是在单个函数或文件范围内的坏代码,那么意大利面条式代码就是整个应用范围内的坏架构。面条可以由单独写得不错的函数组成,但它们之间的交互混乱且不可预测。
术语「意大利面条式代码」出现于 20 世纪 70 年代,伴随着对 goto 语句的批评。在早期的编程语言(BASIC、FORTRAN、COBOL)中,goto 是控制执行流程的主要方式。程序是一系列编号的行,而 goto 允许跳转到其中任意一行。这形成了一个无法解开的跳转「乱麻」。
1968 年,Edsger Dijkstra 发表了著名的信件《Go To Statement Considered Harmful》,这标志着结构化编程时代的开始。Dijkstra 证明了任何算法都可以在不使用 goto 的情况下实现,只需三种结构:顺序、分支(if)和循环(while)。这成为现代编程的基础。
结构化编程并没有完全消除这个问题。意大利面条式代码上升到了新的水平——开发者不再使用物理的 goto,而是开始创建逻辑「goto」:全局变量、JavaScript 中的回调地狱、复杂的调用链以及组件之间隐式的依赖。问题依然存在,只是形式改变了。
JavaScript 中的回调地狱、深层嵌套的 Promise、没有错误处理的 async/await、不知道谁在何时触发的事件——这些都是意大利面条式代码的现代变体。这种反模式依然活跃并不断蔓延,只是现在不再使用 goto 语句了。
缺少分层——第一个也是主要的迹象。在意大利面条式代码中,业务逻辑、数据库操作、HTML 布局和网络交互混杂在同一个文件中,甚至同一个方法中。修改数据库查询可能会破坏 UI 显示,因为这两层代码没有分离。
全局变量和单例——第二个明显迹象。当应用状态存储在全局对象中时,执行流程变得不可预测。任何函数都可能改变全局状态,而几乎不可能追踪这发生在哪里、何时发生。
上帝类和上帝函数——第三个迹象。一个 2000 多行的类,既负责业务逻辑、又负责显示、还负责数据处理——这是典型的意大利面条式代码。一个接收 10 个参数并做 5 件不同事情的函数也是。
| 迹象 | 描述 | 示例 |
|---|---|---|
| 层混合 | UI 代码中的 SQL 查询 | 直接写入数据库的控制器 |
| 全局变量 | 随处可访问的状态 | 每个类中的 static SessionManager |
| 上帝类 | 一个类做所有事情 | 3000 行的 OrderManager |
| 长方法 | 没有拆分的函数 | 200 行且承担 5 项职责的方法 |
| 回调地狱 | 无休止的嵌套回调 | JavaScript 中 6 层嵌套 |
如果您无法在不创建 15 个 mock 对象的情况下为函数编写单元测试——这就是意大利面条式代码。如果测试一个模块需要启动整个应用基础设施——这就是意大利面条式代码。不可测试性是混乱架构的客观指标。
缺少架构设计——最常见的原因。当团队在没有计划的情况下开始写代码,边走边选择架构时,结果不可避免地会变成面条。每个新功能都被添加到「现在方便」的地方,而不是逻辑上应该放的位置。
渐进式发展——第二个原因。项目从一个小的脚本开始,然后增加功能,然后变成应用,再变成单体。与此同时架构没有被重新审视。适用于 100 行代码的东西,对于 10 万行代码就变成了灾难。
违反 SOLID 原则——第三个原因。尤其是单一职责原则(S)和依赖倒置原则(D)。当一个类对一切负责、依赖是硬性的、模块紧密耦合时——就会产生意大利面条式代码。
截止日期和 hotfix 文化——意大利面条式代码的催化剂。当「本来应该昨天就完成」时,开发者会把代码放到随手找到的第一个位置,不考虑架构。十个这样的 hotfix——应用架构就毁了。
主要后果——失去对代码库的控制。开发者不再理解整个应用是如何工作的。一个地方的改动会破坏另一个看似无关的地方。每个补丁都会制造两个新 bug。团队进入「害怕改变」的状态。
团队的生产力呈指数级下降。Microsoft Research(2023)的研究表明,在意大利面条式代码中添加新功能的时间相对于代码库规模呈二次方增长。对于干净的架构,这种增长是线性的。当代码超过 5 万行时,差异变得至关重要。
安全——另一个受害者。在面条式代码中很容易漏掉未处理的异常、错误的输入验证或数据泄露。在架构混乱的项目中进行安全审计几乎不可能——要找到所有使用用户输入的地方是不现实的。
意大利面条式代码项目中的人员流动高于平均水平。经验丰富的开发者离开,因为他们不想处理「面条」。新员工无法理解代码,在前几个月就离职。项目失去专业知识,这进一步降低代码质量——这是一个恶性循环。
第一——从分层开始。将代码分为三个层次:表现层(presentation,UI、控制器)、业务逻辑层(business logic,服务、用例)和数据访问层(data access,仓库、DAO)。即使部分分离也能立即改善结构并使代码可测试。
第二——引入依赖注入。将直接创建依赖改为通过构造函数或参数传递。这打破组件之间的硬性联系,并允许独立测试每个模块。
第三——拆分上帝类和上帝函数。将它们拆成具有单一职责的小类和小的方法。使用 Facade 模式简化复杂的子系统。请记住:20 行的类比 2000 行的类更容易理解。
// 意大利面——一切都在一个方法中
function handleRequest(req, res) {
const db = new Database("mysql://...");
const user = db.query("SELECT * FROM users WHERE id =" + req.params.id);
let html = "";
html += ""
+ user.name + "";
html += "Balance: "
+ user.balance + "";
html += "";
res.send(html);
}
// 干净架构——分离的层
class UserController {
constructor(userService) {
this.userService = userService;
}
async getUser(req, res) {
const user = await this.userService.findById(req.params.id);
res.json(new UserResponse(user));
}
}
class UserService {
constructor(userRepository) {
this.userRepository = userRepository;
}
async findById(id) {
return await this.userRepository.findById(id);
}
}
不要试图一次性重写整个代码库——这注定失败。选择一个模块,为它编写能固定当前行为的测试(特征测试,characterization tests),然后再重构。逐渐地、一个模块一个模块地,您会解开这团面条。
架构规划——预防的基础。在开始开发前确定架构风格:MVC、MVVM、Clean Architecture、VIPER 或其他。编写 ADR(架构决策记录,Architecture Decision Record)说明选择理由。在代码评审中要求遵守架构。
依赖倒置原则(DIP)——对抗意大利面条式代码的有力工具。高层模块不应依赖低层模块。两者都应依赖抽象。依赖注入是这个原则的实际实现。
测试——最好的预防。如果您在写代码之前先写测试(TDD),您必然会设计出低耦合的组件。可测试的代码就是结构良好的代码。不可测试的代码几乎总是意大利面条式代码。
SonarQube——跟踪圈复杂度、继承深度、方法大小。JDepend(Java)——测量包之间的依赖。PhpMetrics——为 PHP 项目提供可维护性指数。在 CI/CD 中关注这些指标——预防面条的出现,而不是事后与它斗争。
常见问题
可以,渐进式重构更可取。使用绞杀者图(Strangler Fig)方法——在不停止应用运行的情况下,逐步用新组件替换旧组件。从分离数据层或业务逻辑开始。在修改前用测试覆盖旧代码,以免丢失功能。
意大利面条式代码是应用所有层的混乱交织。千层面式代码(Lasagna code)是严格的多层架构,但每一层隔离得如此彻底,以至于层间数据传递变成了官僚流程。两种反模式都有害,但意大利面条式代码更危险——它使代码不可预测。
看依赖:如果一个模块从应用所有层导入模块——这很可疑。注意方法的大小——超过 30 行通常不好。检查函数是否混合了 UI 处理、业务逻辑和数据。如果是——这就是意大利面条式代码。
Robert Martin 的 Clean Architecture 和 六边形架构(Hexagonal Architecture,端口与适配器 Ports & Adapters)——是两个最佳方法。两者都能保证层的分离、业务逻辑与框架的独立性以及可测试性。对于移动开发——使用 Repository 模式的 MVVM。
部分可以。圈复杂度(McCabe)、模块耦合度(Coupling)和继承深度(DIT)等指标可以指出潜在的意大利面条式代码。SonarQube、CodeClimate 和 PhpMetrics 会自动计算这些指标。然而完整的诊断需要人工分析架构。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。