后端场景:对“反软件工程范式”架构设计的思考
“反软件工程范式”并不是一个具有统一定义的标准软件工程术语。本文将它作为一个工作概念:指那些表面上使用了先进技术,实际却系统性违背可理解、可验证、可维护、可回滚和可演进原则的架构设计方式。
1. 什么叫“反软件工程”
软件工程不是“把代码写出来”,而是在多人、长期、变化和不确定性条件下,持续交付可靠软件的方法。
因此,判断一种架构是否“反软件工程”,不能只看它是否采用了:
- 微服务;
- DDD;
- 事件驱动;
- Kubernetes;
- Service Mesh;
- AI Agent;
- 中台;
- 低代码平台;
- 各种设计模式。
真正要看的是,这套架构是否让系统更容易:
1 | |
如果一个方案用“先进技术”换来了:
- 更多隐式行为;
- 更长调用链;
- 更模糊的数据所有权;
- 更高认知负担;
- 更难复现的故障;
- 更慢的交付;
- 更危险的变更;
那么它可能在技术名词上很现代,在工程结果上却是倒退。
2. 软件工程的基本目标
可以将工程质量抽象为一个多目标问题:
1 | |
这不是数学公式,而是一种判断框架。任何一个因子接近零,整体结果都很差。
一个系统不是因为“能运行”就完成了工程化。还要问:
- 新人多久能理解?
- 修改一个规则要动几个仓库?
- 出错后能否快速定位?
- 数据错了能否重放和修复?
- 发布失败能否回滚?
- 一项技术不再适用时能否替换?
- 一年后还能否持续开发?
3. 反软件工程架构的共同特征
mindmap
root((反软件工程倾向))
技术先于问题
为微服务而微服务
为事件驱动而事件驱动
为平台化而平台化
复杂度隐形化
魔法注解
隐式上下文
动态规则无边界
边界错误
共享数据库
循环依赖
分布式单体
反馈失效
不可观测
无自动测试
无灰度回滚
责任模糊
数据无所有者
故障无负责人
平台与业务互相甩锅
不可逆决策
大爆炸迁移
厂商锁定
一次性全量重构
这些问题看似不同,根因通常相同:
局部追求技术形式,忽略系统生命周期中的整体成本。
4. 反模式一:技术方案先于业务问题
典型表达:
1 | |
这类设计的顺序反了。
正确顺序应该是:
flowchart LR
P[业务问题] --> C[约束与质量属性]
C --> O[可选方案]
O --> T[权衡与验证]
T --> D[架构决策]
D --> E[小步实施]
E --> F[反馈与演进]
而不是:
flowchart LR
T[先选技术] --> J[寻找使用理由]
J --> A[强行套架构]
A --> C[复杂度扩散]
C --> R[业务为技术买单]
4.1 架构设计应先回答什么
- 系统最核心的业务不变量是什么?
- 当前最大瓶颈是性能、交付、可靠性还是组织协作?
- 哪些变化最频繁?
- 哪些数据必须强一致?
- 哪些故障必须隔离?
- 团队是否具备运维该技术的能力?
- 预期规模是多少,而不是想象中的“互联网级”?
- 方案失败时怎样撤回?
技术应是这些答案的结果,而不是架构评审 PPT 的装饰品。
5. 反模式二:把分布式当作默认答案
单体系统有问题,于是拆微服务;微服务变复杂,于是加网关、注册中心、配置中心、消息队列、链路追踪、服务网格、分布式事务和统一平台。
最后得到一个“分布式单体”:
- 部署上分开;
- 数据上共享;
- 发布上耦合;
- 故障上连锁;
- 开发上跨团队;
- 测试上必须全环境联调。
flowchart LR
A[订单服务] --> DB[(共享数据库)]
B[库存服务] --> DB
C[结算服务] --> DB
D[会员服务] --> DB
A --> B
B --> C
C --> A
D --> B
style DB stroke-width:3px
它承担了分布式系统的全部成本,却没有获得服务自治的收益。
5.1 分布式成本
每跨一次进程边界,都会引入:
- 网络延迟;
- 超时;
- 重试;
- 幂等;
- 部分失败;
- 版本兼容;
- 链路观测;
- 容量隔离;
- 数据一致性;
- 安全认证;
- 测试环境复杂度。
一次本地函数调用和一次远程 RPC 语法可能相似,但工程语义完全不同。
5.2 更稳妥的演进路径
1 | |
flowchart LR
M1[混乱单体] --> M2[模块化单体]
M2 --> B[边界与契约稳定]
B --> S1[必要模块独立服务化]
B --> S2[其余模块继续同进程]
微服务应是边界成熟后的部署选择,而不是用部署边界代替业务建模。
6. 反模式三:共享数据库冒充服务化
两个服务如果可以任意读写对方表,就没有真正的数据自治。
共享数据库导致:
- 表结构修改影响多个团队;
- 业务规则绕过服务 API;
- 无法判断数据所有者;
- 线上问题难以追责和修复;
- 服务不能独立迁移或扩展;
- 事务边界和领域边界混乱。
6.1 正确的数据所有权
flowchart LR
O[订单域] --> ODB[(订单数据)]
I[库存域] --> IDB[(库存数据)]
S[结算域] --> SDB[(结算数据)]
O -->|API/Event| I
O -->|Event| S
I -->|库存确认事件| O
原则:
1 | |
其他模块需要数据时,可以:
- 调用服务 API;
- 订阅领域事件构建本地读模型;
- 使用数据产品或只读副本;
- 通过明确治理的共享主数据服务。
不要让“临时查一下表”成为永久架构。
7. 反模式四:过度抽象与通用化
常见目标:
1 | |
最终常见结果:
- 简单需求也要学习复杂 DSL;
- 业务逻辑分散在配置、脚本、数据库和代码中;
- IDE 无法静态分析;
- 重构和搜索困难;
- 版本发布不可追踪;
- 运行时错误替代编译期错误;
- 平台团队成为所有业务的瓶颈。
7.1 抽象的判断标准
好的抽象应满足:
- 至少有多个已经出现的稳定重复模式;
- 抽象后减少总体概念,而不是增加一套新概念;
- 保留逃生舱,特殊业务可绕开;
- 可测试、可版本化、可观测;
- 使用者不需要理解内部全部实现;
- 删除抽象的成本可控。
不要基于想象中的未来需求设计万能平台。
7.2 Rule of Three
在没有明确证据前,可以先允许少量重复:
1 | |
重复代码不一定比错误抽象更糟。错误抽象会把多个本来独立的变化强行绑在一起。
8. 反模式五:魔法注解和隐式行为
在 Java/Spring 系统中,常见“魔法”包括:
- 一个注解触发鉴权、事务、重试、限流、缓存和消息;
- ThreadLocal 隐式传递租户、用户、事务和 Trace;
- 运行时扫描拼装业务规则;
- 自动配置根据类路径偷偷改变行为;
- AOP 切面顺序影响最终结果;
- 代理导致自调用失效;
- 同一个方法在测试和生产表现不同。
注解不是问题,问题是关键业务语义被隐藏。
8.1 一个危险例子
1 | |
阅读方法体无法知道:
- 锁先拿还是事务先开;
- 重试是否会重复发送事件;
- 异步事件是否在事务提交前发布;
- 事务是否真的生效;
- 幂等记录失败怎样处理;
- 哪个异常会回滚。
8.2 替代原则
关键业务流程应显式表达:
1 | |
并不是所有代码都要写得冗长,而是核心顺序和失败语义应可见、可测试。
9. 反模式六:事件驱动一切
事件驱动适合:
- 解耦非同步完成的后续动作;
- 一对多分发;
- 构建读模型;
- 长流程编排;
- 审计与数据处理。
不适合被滥用为:
- 所有查询都通过事件;
- 用户必须立即知道结果的命令;
- 没有幂等和重放能力的关键写入;
- 仅为了避免定义清晰 API;
- 业务方无法理解的“事件风暴”。
9.1 事件链的隐形耦合
flowchart LR
A[订单创建] --> E1[OrderCreated]
E1 --> B[库存服务]
B --> E2[StockReserved]
E2 --> C[优惠服务]
C --> E3[CouponLocked]
E3 --> D[支付服务]
D --> E4[PaymentPending]
E4 --> F[订单服务]
看似解耦,实际上一个用户动作需要多个消费者按隐式顺序成功。任何一个环节失败都会形成难以理解的中间状态。
9.2 正确使用事件的条件
- 事件定义为已经发生的事实,而不是含糊命令;
- 事件有稳定 schema 和版本策略;
- 消费者幂等;
- 可观察处理进度;
- 支持重放和死信;
- 有业务补偿;
- 明确最终一致窗口;
- 用户可查询当前状态。
10. 反模式七:用分布式事务掩盖边界错误
当一次操作要跨 8 个服务同时强一致,第一反应不应是“上 Seata/XA/TCC”,而应先问:
- 这个业务动作是否被拆得过细?
- 强一致数据是否本应属于同一个聚合或服务?
- 是否可以用状态机和补偿?
- 哪些数据只是派生结果?
- 用户是否真的要求同步完成?
flowchart TD
X[跨 8 服务强事务需求] --> Q{是否边界拆错}
Q -- 是 --> R[重新合并一致性边界]
Q -- 否 --> C{是否可用状态机/补偿}
C -- 是 --> S[Saga/Outbox/可恢复流程]
C -- 否 --> D[谨慎评估分布式事务]
分布式事务可以解决特定问题,但不能替代正确的领域边界。
11. 反模式八:平台化吞噬业务自治
平台建设的初衷通常是复用能力,后期可能演变为:
- 所有团队必须等待平台排期;
- 平台 API 只能满足最低公分母;
- 特殊需求通过大量开关实现;
- 平台不知道业务,业务不能修改平台;
- 故障责任在平台与业务之间来回移动;
- 平台升级要求所有业务同时迁移。
11.1 好平台的标准
好平台更像产品,而不是命令:
1 | |
flowchart LR
Platform[平台能力] --> API[稳定 API/SDK]
Platform --> DOC[文档与示例]
Platform --> OBS[可观测与配额]
Platform --> SELF[自助控制面]
Business[业务团队] --> API
Business --> ESCAPE[必要时的扩展点/逃生舱]
平台不应把内部复杂度全部转嫁给使用者。
12. 反模式九:以“统一”为名消灭合理差异
企业架构经常追求:
- 统一技术栈;
- 统一模型;
- 统一数据库;
- 统一发布;
- 统一权限;
- 统一日志;
- 统一异常码。
统一有价值,但不是越多越好。
应该强统一的是:
- 安全基线;
- 可观测字段;
- 身份认证;
- 数据分类;
- 发布审计;
- 事故响应;
- 基础协议;
- 合规规则。
可以允许差异的是:
- 领域模型;
- 存储选型;
- 内部实现;
- 性能策略;
- 与业务强相关的流程。
正确目标是:
1 | |
13. 反模式十:大爆炸重构
典型计划:
1 | |
风险:
- 旧系统业务细节没有文档;
- 新旧需求同时变化;
- 迁移期间双写和数据对账困难;
- 半年后才得到第一次真实反馈;
- 新系统可能复制旧系统问题;
- 上线窗口不可回滚。
13.1 绞杀者演进
flowchart LR
Traffic[业务流量] --> Facade[统一入口/防腐层]
Facade --> Old[旧系统]
Facade --> New1[新模块 A]
Facade --> New2[新模块 B]
Old -.逐步迁移.-> New1
Old -.逐步迁移.-> New2
每次迁移一个明确能力:
- 建立观测和基线;
- 抽取边界;
- 双读或影子验证;
- 小流量切换;
- 对账;
- 扩大流量;
- 删除旧路径。
架构演进的关键是可逆性,而不是一次性完美。
14. 反模式十一:缺少可观测性的“黑盒架构”
如果系统只能回答“接口报错了”,不能回答:
- 哪个业务单据失败?
- 在哪个阶段失败?
- 调用了哪些下游?
- 重试了几次?
- 状态是否已部分提交?
- 是否可以安全重放?
那么架构再先进也不可运营。
14.1 三层可观测性
flowchart TD
I[基础设施层] --> M1[CPU/内存/网络/磁盘]
A[应用层] --> M2[QPS/延迟/错误/依赖]
B[业务层] --> M3[订单/结算/履约状态与不变量]
业务系统必须有业务级指标和日志字段,例如:
1 | |
只有技术指标,没有业务状态,事故现场仍然难以恢复。
15. 反模式十二:把“高可用”理解为组件数量多
部署三套 Redis、五个注册中心、十个服务副本,不等于系统高可用。
高可用要从用户路径分析:
flowchart LR
U[用户请求] --> G[网关]
G --> A[应用服务]
A --> D[数据库]
A --> R[缓存]
A --> M[消息系统]
G -.故障是否降级.-> U
R -.故障是否可回源.-> A
M -.故障是否可延迟.-> A
D -.故障是否可切换.-> A
要问:
- 单点在哪里?
- 超时预算是多少?
- 依赖故障是否会级联?
- 缓存不可用能否回源?
- MQ 不可用时核心交易是否可继续?
- 降级是否经过演练?
- 数据恢复点 RPO 和恢复时间 RTO 是多少?
“组件高可用”不等于“业务链路高可用”。
16. AI 编程带来的新型反软件工程风险
AI 可以快速生成大量代码,但“代码产量”不是软件工程产出。
新风险包括:
- 生成多个风格相似但语义不同的实现;
- 复制过时 API 和不安全模式;
- 在不了解领域不变量时修改核心逻辑;
- 为了通过测试而修改测试;
- 生成看似合理但无法解释的抽象;
- 代码规模增长快于团队理解速度;
- Prompt 和 Agent 行为缺少版本治理;
- 同一任务不同运行结果漂移。
16.1 AI 时代更需要工程约束
flowchart LR
Intent[人定义意图与约束] --> Agent[AI 生成/修改]
Agent --> Test[自动测试与静态检查]
Test --> Review[架构/安全/领域评审]
Review --> Deploy[渐进式发布]
Deploy --> Observe[运行反馈]
Observe --> Intent
需要建立:
- 架构规则自动检查;
- 测试先行或至少测试伴随;
- 代码所有权;
- 依赖和许可证检查;
- 安全扫描;
- AI 修改范围限制;
- 可重放的任务上下文;
- 人对关键业务决策负责。
AI 应降低实现成本,但不能取消验证、责任和反馈闭环。
17. 如何判断一个架构方案是否健康
可以使用以下评审矩阵。
| 维度 | 核心问题 |
|---|---|
| 业务正确性 | 核心不变量是否明确? |
| 认知负担 | 一个团队能否理解并独立维护? |
| 变更成本 | 常见需求要改多少模块和仓库? |
| 数据所有权 | 每个事实是否有唯一权威写入者? |
| 故障隔离 | 一个模块故障是否会拖垮全链路? |
| 可测试性 | 能否在不启动全世界的情况下验证? |
| 可观测性 | 能否定位到业务阶段和数据状态? |
| 可恢复性 | 失败后能否重试、补偿、重放、对账? |
| 可逆性 | 技术选择或发布失败能否撤回? |
| 性能与容量 | 是否有基于数据的容量模型? |
| 安全与合规 | 权限、审计、隐私是否内建? |
| 演进能力 | 边界是否允许逐步替换? |
17.1 一个简化评分模型
1 | |
不要只评估吞吐和延迟。大多数企业系统更常死于变更困难和故障不可恢复,而不是 CPU 不够。
18. 推荐的架构设计流程
第一步:写清业务场景和不变量
例如结算系统:
1 | |
第二步:定义质量属性场景
不要写“高可用、高性能”,而要写:
1 | |
第三步:识别变化轴
- 哪些规则经常变?
- 哪些模块由不同团队维护?
- 哪些数据量增长最快?
- 哪些能力需要独立部署?
第四步:设计最小充分架构
选择能满足当前可预见需求的最简单方案,并保留扩展缝隙。
第五步:记录 ADR
1 | |
第六步:建立架构适应度函数
通过自动化持续验证:
- 模块依赖不能反向;
- 核心域不能依赖基础设施实现;
- 禁止跨域直接访问表;
- API 兼容性;
- 延迟和错误预算;
- 包大小、循环依赖、许可证和漏洞。
第七步:小步上线并观察
架构不是评审通过就正确,必须经过真实流量反馈。
19. 一个典型案例:从“万能中台”回到可演进架构
假设公司建设统一订单中台,试图覆盖电商、门店、订阅、服务预约和联营结算。
初期模型:
1 | |
几年后出现:
- 数百个 nullable 字段;
- 大量
biz_type分支; - 一个发布影响所有业务;
- 任何新业务都要等中台改模型;
- 中台既不知道业务细节,又掌控所有数据;
- 各业务偷偷建立影子表和旁路接口。
这不是“统一成功”,而是边界失真。
19.1 改造思路
flowchart TD
U[万能订单中台] --> K[提炼真正稳定的内核]
K --> ID[统一身份与编号]
K --> AUDIT[审计规范]
K --> EVENT[基础事件协议]
U --> E[电商订单域]
U --> S[订阅订单域]
U --> R[预约订单域]
U --> F[结算域]
E -->|稳定契约| K
S -->|稳定契约| K
R -->|稳定契约| K
F -->|订阅业务事实| E
统一真正稳定的基础能力,业务差异回到各领域自治。不是“不要中台”,而是避免中台吞并领域。
20. 架构师应防止的思维偏差
20.1 幸存者偏差
大厂使用某技术,不代表你的规模、团队和问题相同。
20.2 简历驱动开发
为了让技术栈好看,引入业务不需要的复杂组件。
20.3 沉没成本
平台已经投入很多,所以即使不适合也不能停止。
20.4 控制幻觉
觉得所有业务统一到平台后就“可控”,实际复杂性只是被隐藏。
20.5 过早优化
没有压测和数据,先为假想规模设计极端架构。
20.6 局部最优
一个团队开发更快,却让十个下游团队集成更慢。
20.7 权威偏差
架构师或专家提出的方案缺少验证,但团队不敢质疑。
21. 怎样在面试中回答
面试官提出“你如何看待反软件工程范式”时,可以按以下结构:
- 先说明该词不是统一标准术语,并给出工作定义;
- 强调软件工程关注生命周期总成本,不是技术先进程度;
- 举出三类典型反模式:分布式单体、过度抽象、隐式魔法;
- 分析根因:技术先行、边界错误、反馈不足、组织责任模糊;
- 给出替代路线:问题驱动、模块化、数据自治、显式契约、演进式迁移;
- 用可测试、可观测、可恢复、可逆等指标评审;
- 说明 AI 时代代码生成越快,工程约束越重要。
22. 面试总结话术
我把“反软件工程范式”理解为:表面采用先进架构,实际却增加偶然复杂度、隐藏关键语义、破坏边界并削弱反馈能力的设计方式。典型表现包括技术先于问题、分布式单体、共享数据库、过度平台化、事件驱动一切、魔法注解和大爆炸重构。判断架构不能只看组件和性能,还要看认知负担、变更成本、数据所有权、故障隔离、可测试、可观测、可恢复和可逆性。更健康的路线是先明确业务不变量和质量属性,以模块化单体或最小充分架构起步,边界成熟后再拆分,通过 ADR、架构适应度函数、渐进发布和运行反馈持续演进。AI 能提高代码产量,但不会自动提高工程质量,反而更需要约束和验证。
23. 延伸追问
- 如何判断一个模块应该拆成微服务?
- 共享数据库一定错误吗?什么阶段可以接受?
- 如何量化团队认知负担?
- 平台化和业务自治如何平衡?
- 如何识别错误抽象?
- 事件驱动和同步 RPC 如何选择?
- 架构治理如何避免变成审批官僚主义?
- AI Agent 生成代码后,责任边界如何定义?
- 如何从分布式单体逐步修复,而不是再次大重构?
参考资料
- Martin Fowler, Microservices: https://martinfowler.com/articles/microservices.html
- Martin Fowler, Strangler Fig Application: https://martinfowler.com/bliki/StranglerFigApplication.html
- AWS Well-Architected Framework: https://docs.aws.amazon.com/wellarchitected/latest/framework/welcome.html
- Google SRE Books: https://sre.google/books/
- C4 Model: https://c4model.com/