副作用驱动设计体系(Effect-Driven Design, EDD)
大约 4 分钟
Title
副作用驱动设计体系(Effect-Driven Design, EDD)
一、设计哲学
副作用驱动设计(Effect-Driven Design, EDD)是一种以“副作用识别与组织”为核心的系统架构方法。它不以MVC、DDD等传统技术结构为起点,而以副作用的边界、风险和组织方式为第一性原则,构建适应变化、具备可观测性和可测试性的现代系统。
核心口号:副作用不可怕,不可控的副作用才可怕。
二、核心构成模块
| 层级 | 内容 | 说明 |
|---|---|---|
core/ | 纯业务数据结构与逻辑 | 不允许副作用,可100%测试 |
domain/ | 行为语义接口定义(trait)(行为定义层) | 描述副作用语义,不绑定具体框架 |
app/ | usecase编排层(这个其实应该是Service层) | 组合 trait、协调行为、返回结果+事件 |
infra/ | 具体实现,如 DB/API/Cache (这个是副作用实现层) | 所有副作用实现汇聚于此层 |
event/ | 事件定义与事件调度器 (这是副作用触发层) | 用于解耦外部副作用与主流程 |
| 代码模板结构: |
edd-template/
├── src/
│ ├── core/ # 纯逻辑与模型定义
│ ├── domain/ # trait 抽象接口
│ ├── app/ # usecase 调用与事件组织
│ ├── infra/ # SeaORM 数据库实现、EventDispatcher
│ ├── event/ # AppEvent + OutboxManager
│ └── main.rs # 项目入口
├── migrations/ # SeaORM migration 模板
├── README.md
├── Cargo.toml
└── template.toml # cargo-generate metadata
三、副作用评判标准(Effect Heuristics)
| 维度 | 问题 | 高风险信号 | 建议 |
|---|---|---|---|
| 影响范围 | 是否改变状态? | 写库、调用RPC | 用 trait 封装 |
| 可测性 | 是否能mock? | 外部网络/环境/时间依赖 | 分离纯逻辑 + 抽象副作用层 |
| 幂等性 | 重复调用是否影响? | 积分重复发、重复入账 | 接入幂等键 / 事件幂等控制 |
| 顺序敏感性 | 调用顺序是否重要? | 发券→发通知 | 引入队列 / job scheduler |
| 用户感知性 | 失败是否对用户可感知? | 没有收到欢迎邮件 | 记录日志或提升为事件 |
| 失败代价 | 失败是否需要补偿? | 扣款、业务激活 | 用事件队列确保最终一致 |
四、usecase输出模型
pub struct Outcome<T> {
pub data:T,
pub from_case:AppUseCase,
pub events:Vec<AppEvent>
}
usecase 中只进行数据库写入与业务流程判断, 所有发消息、发积分、第三方通知等副作用行为转化为 AppEvent,由外部 orchestrator 异步处理。
五、事件设计范式(Outbox Pattern)
outbox 表结构
CREATE TABLE outbox_events (
id UUID PRIMARY KEY,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMP DEFAULT now(),
dispatched BOOLEAN DEFAULT FALSE
);
消费器 worker
// 每5秒轮询未发送事件并调度
六、副作用分类与架构策略
| 类型 | 示例 | 架构建议 |
|---|---|---|
| 决策副作用 | 用户是否黑名单、查重 | trait 注入 + mockable |
| 执行副作用 | 发积分、发消息 | usecase 内记录 event,outbox 外发 |
| 异步非关键副作用 | 日志、埋点、追踪 | 用完即删,记录失败不处理 |
| 外部状态操作 | 三方API、风控策略调用 | 异步 + 幂等,必要时引入补偿机制 |
| 时间/上下文依赖 | now()、UUID、request-ip | 抽象 Clock / IdGenerator / ContextProvider trait (问题同上) |
七、开发流程指南
- 识别 usecase 的副作用点
- 将所有副作用提炼为 trait 或 event
- 保持 usecase 的
async fn input -> Outcome<T>签名,必要时,需要套上一个AppResult. - 用事件调度器处理外发任务
- 编写单元测试 + integration test + event replay 测试
八、副作用治理优先级矩阵(简化)
| 优先级 | 特征 | 示例 | 建议 |
|---|---|---|---|
| P0 | 失败即破坏业务 | 扣款、发票 | 事务+幂等+事件重放 |
| P1 | 有失败影响但可补偿 | 发积分、发通知 | Outbox + 重试机制 |
| P2 | 用户无感知 | 打点、埋点 | fire-and-forget,无需回滚 |
九、结语
Effect-Driven Design 不替代传统分层架构,但它提供了副作用识别、组织与调度的第一性方法论,是未来以异步+分布式+高一致性为常态的系统中重要的架构思想补充。