编程中的弗兰肯斯坦是指由不同技术、风格和架构中互不兼容的部分拼凑而成的代码。根据ThoughtWorks Technology Radar(2024)的研究,28%的大型项目带有弗兰肯斯坦综合症的迹象——即在缺乏统一技术愿景时出现的架构折衷主义。类比玛丽·雪莱的小说,这样的代码能够运行,但其维护却会变成一场噩梦。
要点
弗兰肯斯坦(Frankenstein code、Frankenstein pattern)是一种反模式,指软件系统由本不打算协同工作的部分拼装而成。就像弗兰肯斯坦的怪物一样,这样的代码可以运转,但它丑陋、不可预测,哪怕最微小的改动也会带来危险。
这个词来自文学:在玛丽·雪莱的小说《弗兰肯斯坦,或现代的普罗米修斯》(1818年)中,科学家用不同逝者的身体碎片创造了一个活物。在编程中,这种类比非常贴切——开发者从不同的框架、库、语言中取来片段,用“活线”将它们缝合在一起,得到的是能运行却骇人的结果。
弗兰肯斯坦与意大利面代码的区别在于问题的规模和性质。意大利面代码是单一技术栈内部的混乱结构。弗兰肯斯坦是架构层面的折衷主义:不同的技术、不兼容的范式、同一系统内部相互冲突的方法。
微服务架构允许不同的服务使用不同的技术,但前提是有清晰的边界和标准化的交互协议。弗兰肯斯坦则是没有边界的混乱混合:同一个控制器里既有 REST 又有 GraphQL,同一个模块里有两套 ORM,同一个实体既用 SQL 又用 NoSQL。
缺少技术负责人或架构师是根本原因。当项目中没有人为架构的完整性负责时,每个开发者都会选择“适合自己的”工具。有人喜欢 Spring,有人喜欢 Guice,还有人用自己写的 DI。结果就是架构大杂烩。
项目合并是第二个常见原因。两个团队各自独立开发自己的模块,使用不同的技术栈。当需要把这些模块合并到同一个应用时,它们只是用适配器和中间层“粘”在一起,就变成了弗兰肯斯坦。
企业并购是第三种情形。A 公司收购了 B 公司,想把它的产品整合进自己的体系。与其重写,不如通过 API、共享数据库和各种补丁拼接。一年后,系统就变成了无人理解的怪兽。
| 原因 | 描述 | 典型结果 |
|---|---|---|
| 没有架构师 | 每个开发者选择自己的技术栈 | 一个模块里有3个不同的 HTTP 客户端 |
| 项目合并 | 两个产品拼成一个 | 两套 ORM、两种日志记录方式 |
| M&A | 并购公司及其产品 | 不同架构和风格的混合体 |
| 试验 | 无策略地引入新技术 | 一个文件里同时有 Java 8 和 Java 21 的特性 |
| 政治决策 | 从上而下强加技术而不考虑背景 | 为简单脚本使用企业级框架 |
有经验的开发者想在生产环境中尝试新技术,往往成为弗兰肯斯坦的源头。他们没有把试验限制在隔离的模块中,而是把试验性代码引入系统的关键部分。
经典例子是在一个应用中使用多套 ORM。部分模块用 Hibernate,部分用 MyBatis,还有部分直接写 JDBC 查询。事务变得难以管理,缓存变得不一致,新开发者也不知道新功能该用哪种方式。
第二个例子是混合架构风格。REST API 的控制器里出现了 SOAP 服务调用、直接 SQL 查询、文件系统访问和 HTML 生成。这样的应用无法测试、无法扩展,也无法编写文档。
第三个例子是技术栈:后端用 Python,微服务用 Node.js,桌面客户端用 C#,Android 应用用 Java,而所有业务逻辑都被摊薄在它们之间,没有清晰的责任划分。
// 弗兰肯斯坦——混合的风格和技术
// callbacks、Promises 和 async/await 混用
// callbacks
db.query("SELECT * FROM users", function(err, rows) {
if (err) handleError(err);
// 回调里的 Promise
fetch("/api/data").then(function(data) {
// then 里的 async/await
(async () => {
const result = await processData(data);
sendResponse(result);
})();
});
});
// 干净的代码——统一的 async/await 风格
async function getUserData(userId) {
const user = await db.query("SELECT * FROM users WHERE id = ?", [userId]);
const data = await fetch("/api/data/" + userId);
return await processData(user, data);
}
同一个数据库同时被当作 SQL 关系型(带规范化)和 NoSQL 文档型(带 JSON 列)来使用。部分查询走 ORM,部分走存储过程,部分从代码里直接写 SQL。数据库 schema 没有文档,迁移相互冲突。
入职难度是第一个后果。新开发者必须掌握 5 门语言、3 个框架、2 种架构风格,才能理解系统是如何运行的。入职时间从几周拖到几个月。根据 LinkedIn(2023)的数据,技术折衷主义的项目流失新员工的概率高出 2 倍。
行为不可预测是第二个后果。对 Python 微服务的改动可能会意外弄坏 Java 模块,因为它们共享同一个数据库却没有明确的契约。调试这类问题需要同时掌握技术栈中的全部技术。
安全性是第三个后果。技术栈中的每项技术都需要自己的安全配置、自己的补丁、自己的监控。要在可接受的水平上维护 5–6 项异构技术几乎不可能,其中一项必然存在漏洞。
SonarQube 可以衡量技术债,却无法衡量“架构债”——即组件之间的不兼容。这种债务并不体现在 lint 工具的警告里,而是体现在:不修改三个用不同技术写的模块,就无法添加一个新功能。
第一也是最重要的一步:指定一位负责技术栈完整性的架构师或 tech lead。这个人对未经架构评审的新技术拥有一票否决权。不是民主,而是对关键技术做出负责任的最终决定。
第二步:推行 Architecture Decision Record(ADR)流程。任何重要的架构决策(选择数据库、框架、协议)都以简短文本记录下来:背景、考虑过的备选方案、最终决定、影响。ADR 保存在仓库中,向整个团队开放。
第三步:确立“一个任务——一种工具”的原则。HTTP 请求用同一个客户端,ORM 用同一个库,日志记录用同一个框架。例外只能通过有充分理由的 ADR 批准。如果项目里已经有 Axios,就不要再加 fetch;如果有 SLF4J,就不要用 System.out 输出。
试验是允许的,但要在隔离的环境中。划出一个模块或服务,可以在不影响系统其他部分的情况下用新技术重写。试验成功,就通过 ADR 把它标准化;失败,就无痛删除。
盘点是第一步。画出完整的技术栈地图:用了哪些框架、库、语言、协议,在哪些模块、为哪些任务使用。你会看到问题的规模:工具重复、技术冲突、无用的依赖。
标准化是第二步。为每类任务选一种工具。例如:ORM 只用 Hibernate,日志只用 SLF4J + Logback,API 只用 REST。把标准写进 ADR。从折衷主义问题最严重的模块开始替换。
Parallel Run(并行运行)策略是第三步。旧工具和新工具并行工作,直到新工具证明自己的可靠性。例如,旧 HTTP 客户端和新客户端同时运行,但新的只处理一部分请求。经过稳定期后删除旧客户端。
// 弗兰肯斯坦——一个项目里三种 HTTP 方式
// 模块 A:OkHttp
OkHttpClient client = new OkHttpClient().newCall(request);
// 模块 B:RestTemplate (Spring)
restTemplate.getForObject(url, String.class);
// 模块 C:java.net.HttpURLConnection
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
// 统一方案:同步用 RestTemplate,响应式用 WebClient
@Autowired
private RestTemplate restTemplate;
public String callApi(String url) {
return restTemplate.getForObject(url, String.class);
}
技术负责人是对抗弗兰肯斯坦的主要工具。不是经理,不是象牙塔里的架构师,而是真正写代码、评审 PR、做架构决策的一线开发者。没有这样的人,项目不可避免地滑向技术折衷主义。
RFC(Request for Comments)是借鉴自开源社区的过程。在引入任何重要技术之前,作者先写 RFC:问题、提议的解决方案、备选方案、实施计划。团队讨论、投票、通过或否决。RFC 带来透明度,避免“悄悄”做出架构决策。
技术雷达(ThoughtWorks 的 Technology Radar)是技术分类工具:Adopt、Trial、Assess、Hold。团队定期复查雷达并更新状态。这有助于区分“时髦”与“有用”,避免把未经检验的技术引入关键代码。
架构最重要的品质是一致性(consistency)。即便不是最好的工具,只要在整个项目中使用,也比只在单个模块中使用的最佳工具更好。一致性降低认知负担,简化入职,让代码可预测。
常见问题
Polyglot persistence 是刻意地为不同任务使用不同的数据库(PostgreSQL 处理事务、Redis 做缓存、Elasticsearch 做搜索)。弗兰肯斯坦是没有策略的混乱混合。区别在于是否存在架构决策:polyglot 是计划,弗兰肯斯坦是计划的缺失。
会,而且这是常见问题。当每个微服务都使用自己的语言、自己的数据库、自己的协议、自己的部署方式,又没有统一标准时,就出现了分布式弗兰肯斯坦。微服务需要共同的标准:统一的协议(REST/gRPC)、统一的日志格式、集中式的可观测性(observability)。
不要禁止,而要引导。请作者写 RFC:说明为什么现有方案不合适、考虑了哪些备选方案、如何迁移。往往在写 RFC 的过程中,开发者自己就会明白新技术并不必要。如果 RFC 有说服力,就采纳,但要带着计划和限制。
先盘点,再标准化。不要试图一次性重写所有东西。划出一层(例如 HTTP 客户端或日志),选择统一工具,写 ADR,然后逐步迁移。Strangler Fig 方法——在不停止应用运行的前提下,把旧组件逐个替换为新组件。
越少越好。理想情况是一门语言、一个框架、一个数据库、一种日志方式。现实情况是 2–3 门语言(要有清晰划分)、1–2 个数据库、1–2 个框架。每多一项技术都会增加团队的认知负担和维护成本。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。