后端场景:对“反软件工程范式”架构设计的思考

“反软件工程范式”并不是一个具有统一定义的标准软件工程术语。本文将它作为一个工作概念:指那些表面上使用了先进技术,实际却系统性违背可理解、可验证、可维护、可回滚和可演进原则的架构设计方式。

1. 什么叫“反软件工程”

软件工程不是“把代码写出来”,而是在多人、长期、变化和不确定性条件下,持续交付可靠软件的方法。

因此,判断一种架构是否“反软件工程”,不能只看它是否采用了:

  • 微服务;
  • DDD;
  • 事件驱动;
  • Kubernetes;
  • Service Mesh;
  • AI Agent;
  • 中台;
  • 低代码平台;
  • 各种设计模式。

真正要看的是,这套架构是否让系统更容易:

1
理解、修改、测试、部署、观察、恢复、替换和演进

如果一个方案用“先进技术”换来了:

  • 更多隐式行为;
  • 更长调用链;
  • 更模糊的数据所有权;
  • 更高认知负担;
  • 更难复现的故障;
  • 更慢的交付;
  • 更危险的变更;

那么它可能在技术名词上很现代,在工程结果上却是倒退。

2. 软件工程的基本目标

可以将工程质量抽象为一个多目标问题:

1
2
3
4
5
6
7
Engineering Value
= Business Value Delivery
× Reliability
× Maintainability
× Operability
× Evolvability
÷ Total Complexity Cost

这不是数学公式,而是一种判断框架。任何一个因子接近零,整体结果都很差。

一个系统不是因为“能运行”就完成了工程化。还要问:

  • 新人多久能理解?
  • 修改一个规则要动几个仓库?
  • 出错后能否快速定位?
  • 数据错了能否重放和修复?
  • 发布失败能否回滚?
  • 一项技术不再适用时能否替换?
  • 一年后还能否持续开发?

3. 反软件工程架构的共同特征

mindmap
  root((反软件工程倾向))
    技术先于问题
      为微服务而微服务
      为事件驱动而事件驱动
      为平台化而平台化
    复杂度隐形化
      魔法注解
      隐式上下文
      动态规则无边界
    边界错误
      共享数据库
      循环依赖
      分布式单体
    反馈失效
      不可观测
      无自动测试
      无灰度回滚
    责任模糊
      数据无所有者
      故障无负责人
      平台与业务互相甩锅
    不可逆决策
      大爆炸迁移
      厂商锁定
      一次性全量重构

这些问题看似不同,根因通常相同:

局部追求技术形式,忽略系统生命周期中的整体成本。

4. 反模式一:技术方案先于业务问题

典型表达:

1
2
3
4
“我们要上微服务,所以先拆 50 个服务。”
“我们要事件驱动,所以所有接口都改成 MQ。”
“我们要做中台,所以所有业务都必须接入统一模型。”
“我们要使用 AI,所以每个流程都加一个 Agent。”

这类设计的顺序反了。

正确顺序应该是:

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 架构设计应先回答什么

  1. 系统最核心的业务不变量是什么?
  2. 当前最大瓶颈是性能、交付、可靠性还是组织协作?
  3. 哪些变化最频繁?
  4. 哪些数据必须强一致?
  5. 哪些故障必须隔离?
  6. 团队是否具备运维该技术的能力?
  7. 预期规模是多少,而不是想象中的“互联网级”?
  8. 方案失败时怎样撤回?

技术应是这些答案的结果,而不是架构评审 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
2
3
4
5
结构清晰的单体
-> 模块化单体
-> 明确数据边界
-> 识别独立扩展/故障隔离需求
-> 只拆真正需要独立生命周期的模块
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
2
3
“做一个万能流程引擎,支持未来所有业务。”
“做一个统一规则平台,所有判断都配置化。”
“做一个通用 CRUD 框架,业务不再写代码。”

最终常见结果:

  • 简单需求也要学习复杂 DSL;
  • 业务逻辑分散在配置、脚本、数据库和代码中;
  • IDE 无法静态分析;
  • 重构和搜索困难;
  • 版本发布不可追踪;
  • 运行时错误替代编译期错误;
  • 平台团队成为所有业务的瓶颈。

7.1 抽象的判断标准

好的抽象应满足:

  1. 至少有多个已经出现的稳定重复模式;
  2. 抽象后减少总体概念,而不是增加一套新概念;
  3. 保留逃生舱,特殊业务可绕开;
  4. 可测试、可版本化、可观测;
  5. 使用者不需要理解内部全部实现;
  6. 删除抽象的成本可控。

不要基于想象中的未来需求设计万能平台。

7.2 Rule of Three

在没有明确证据前,可以先允许少量重复:

1
2
3
第一次:直接实现
第二次:观察相似性
第三次:提炼稳定抽象

重复代码不一定比错误抽象更糟。错误抽象会把多个本来独立的变化强行绑在一起。

8. 反模式五:魔法注解和隐式行为

在 Java/Spring 系统中,常见“魔法”包括:

  • 一个注解触发鉴权、事务、重试、限流、缓存和消息;
  • ThreadLocal 隐式传递租户、用户、事务和 Trace;
  • 运行时扫描拼装业务规则;
  • 自动配置根据类路径偷偷改变行为;
  • AOP 切面顺序影响最终结果;
  • 代理导致自调用失效;
  • 同一个方法在测试和生产表现不同。

注解不是问题,问题是关键业务语义被隐藏。

8.1 一个危险例子

1
2
3
4
5
6
7
8
@FinanceTransactional
@Retryable
@Idempotent
@DistributedLock
@AsyncEvent
public void settle(SettlementCommand command) {
// 看似只有三行,实际运行语义可能跨越多个系统
}

阅读方法体无法知道:

  • 锁先拿还是事务先开;
  • 重试是否会重复发送事件;
  • 异步事件是否在事务提交前发布;
  • 事务是否真的生效;
  • 幂等记录失败怎样处理;
  • 哪个异常会回滚。

8.2 替代原则

关键业务流程应显式表达:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public SettlementResult settle(SettlementCommand command) {
IdempotencyResult previous = idempotencyService.find(command.requestId());
if (previous != null) {
return previous.result();
}

return transactionTemplate.execute(status -> {
Settlement settlement = settlementRepository.lockAndLoad(command.billId());
SettlementResult result = settlement.confirm(command);
outboxRepository.append(SettlementConfirmed.from(result));
idempotencyService.save(command.requestId(), result);
return result;
});
}

并不是所有代码都要写得冗长,而是核心顺序和失败语义应可见、可测试。

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
2
3
4
5
6
7
8
自助使用
清晰契约
默认安全
可观测
版本兼容
有逃生舱
低接入成本
能证明减少了总成本
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

每次迁移一个明确能力:

  1. 建立观测和基线;
  2. 抽取边界;
  3. 双读或影子验证;
  4. 小流量切换;
  5. 对账;
  6. 扩大流量;
  7. 删除旧路径。

架构演进的关键是可逆性,而不是一次性完美。

14. 反模式十一:缺少可观测性的“黑盒架构”

如果系统只能回答“接口报错了”,不能回答:

  • 哪个业务单据失败?
  • 在哪个阶段失败?
  • 调用了哪些下游?
  • 重试了几次?
  • 状态是否已部分提交?
  • 是否可以安全重放?

那么架构再先进也不可运营。

14.1 三层可观测性

flowchart TD
    I[基础设施层] --> M1[CPU/内存/网络/磁盘]
    A[应用层] --> M2[QPS/延迟/错误/依赖]
    B[业务层] --> M3[订单/结算/履约状态与不变量]

业务系统必须有业务级指标和日志字段,例如:

1
2
3
4
5
6
7
8
9
10
11
biz
scene
step
phase
bill_type
bill_id
request_id
trace_id
result
cost_ms
error_code

只有技术指标,没有业务状态,事故现场仍然难以恢复。

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
2
3
4
5
6
7
8
9
Architecture Score
= Changeability
+ Reliability
+ Observability
+ Recoverability
+ Security
+ Team Fit
- Accidental Complexity
- Irreversibility

不要只评估吞吐和延迟。大多数企业系统更常死于变更困难和故障不可恢复,而不是 CPU 不够。

18. 推荐的架构设计流程

第一步:写清业务场景和不变量

例如结算系统:

1
2
3
4
同一结算单不能重复确认
确认后的金额变化必须可追溯
账期不能非法重叠
所有财务分录必须平衡

第二步:定义质量属性场景

不要写“高可用、高性能”,而要写:

1
2
3
4
5
月末峰值 500 QPS
P99 小于 300ms
单个下游超时不能导致线程池耗尽
数据库主节点故障 5 分钟内恢复
核心交易 RPO 接近 0

第三步:识别变化轴

  • 哪些规则经常变?
  • 哪些模块由不同团队维护?
  • 哪些数据量增长最快?
  • 哪些能力需要独立部署?

第四步:设计最小充分架构

选择能满足当前可预见需求的最简单方案,并保留扩展缝隙。

第五步:记录 ADR

1
2
3
4
5
6
7
背景
决策
备选方案
权衡
后果
回退方式
复审时间

第六步:建立架构适应度函数

通过自动化持续验证:

  • 模块依赖不能反向;
  • 核心域不能依赖基础设施实现;
  • 禁止跨域直接访问表;
  • API 兼容性;
  • 延迟和错误预算;
  • 包大小、循环依赖、许可证和漏洞。

第七步:小步上线并观察

架构不是评审通过就正确,必须经过真实流量反馈。

19. 一个典型案例:从“万能中台”回到可演进架构

假设公司建设统一订单中台,试图覆盖电商、门店、订阅、服务预约和联营结算。

初期模型:

1
2
3
4
UniversalOrder
UniversalItem
UniversalPromotion
UniversalSettlement

几年后出现:

  • 数百个 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. 怎样在面试中回答

面试官提出“你如何看待反软件工程范式”时,可以按以下结构:

  1. 先说明该词不是统一标准术语,并给出工作定义;
  2. 强调软件工程关注生命周期总成本,不是技术先进程度;
  3. 举出三类典型反模式:分布式单体、过度抽象、隐式魔法;
  4. 分析根因:技术先行、边界错误、反馈不足、组织责任模糊;
  5. 给出替代路线:问题驱动、模块化、数据自治、显式契约、演进式迁移;
  6. 用可测试、可观测、可恢复、可逆等指标评审;
  7. 说明 AI 时代代码生成越快,工程约束越重要。

22. 面试总结话术

我把“反软件工程范式”理解为:表面采用先进架构,实际却增加偶然复杂度、隐藏关键语义、破坏边界并削弱反馈能力的设计方式。典型表现包括技术先于问题、分布式单体、共享数据库、过度平台化、事件驱动一切、魔法注解和大爆炸重构。判断架构不能只看组件和性能,还要看认知负担、变更成本、数据所有权、故障隔离、可测试、可观测、可恢复和可逆性。更健康的路线是先明确业务不变量和质量属性,以模块化单体或最小充分架构起步,边界成熟后再拆分,通过 ADR、架构适应度函数、渐进发布和运行反馈持续演进。AI 能提高代码产量,但不会自动提高工程质量,反而更需要约束和验证。

23. 延伸追问

  1. 如何判断一个模块应该拆成微服务?
  2. 共享数据库一定错误吗?什么阶段可以接受?
  3. 如何量化团队认知负担?
  4. 平台化和业务自治如何平衡?
  5. 如何识别错误抽象?
  6. 事件驱动和同步 RPC 如何选择?
  7. 架构治理如何避免变成审批官僚主义?
  8. AI Agent 生成代码后,责任边界如何定义?
  9. 如何从分布式单体逐步修复,而不是再次大重构?

参考资料


后端场景:对“反软件工程范式”架构设计的思考
https://allendericdalexander.github.io/2026/07/29/archtect/examples/06-thinking-about-anti-software-engineering-architecture/
作者
AtLuoFu
发布于
2026年7月29日
许可协议