EDD 副作用驱动开发
大约 7 分钟
Effect-Driven Design 副作用驱动设计
一、从副作用说起
什么是副作用?
在编程中,副作用(Side Effect) 是指函数或操作除了返回值之外,对系统状态产生的任何可观察的改变。
// 纯函数 - 无副作用
fn calculate_price(quantity: u32, unit_price: f64) -> f64 {
quantity as f64 * unit_price
}
// 有副作用的函数
async fn create_order(items: Vec<Item>) -> Result<Order> {
let order = Order::new(items);
database.insert(&order).await?; // 副作用:数据库写入
email.send_confirmation(&order).await?; // 副作用:发送邮件
Ok(order)
}
Web 开发中的常见副作用
- I/O 操作:数据库读写、文件操作
- 网络请求:调用外部 API、发送消息
- 状态变更:修改全局变量、缓存更新
- 时间依赖:获取当前时间、定时任务
- 外部交互:发送通知、记录日志
二、主流架构的副作用困境
MVC:混乱的开始
// 典型的 MVC Controller
impl UserController {
async fn register(&self, input: RegisterInput) -> Result<Response> {
// 验证逻辑与副作用交织
if self.db.exists_email(&input.email).await? {
return Err("Email already exists");
}
// 创建用户
let user = User::new(input);
self.db.save_user(&user).await?;
// 副作用散落各处
self.email_service.send_welcome(&user).await?;
self.analytics.track("user_registered", &user).await?;
Ok(Response::success(user))
}
}
问题:业务规则与副作用紧密耦合,难以测试和理解。
DDD:理想与现实的差距
// Domain 层保持纯净
impl User {
pub fn new(email: Email, name: String) -> Result<Self> {
// 纯业务逻辑
validate_name(&name)?;
Ok(Self { id: UserId::new(), email, name })
}
}
// Application Service 成为副作用垃圾场
impl UserService {
async fn register(&self, cmd: RegisterCommand) -> Result<User> {
let user = User::new(cmd.email, cmd.name)?;
// 副作用堆积
if self.repo.exists_email(&user.email).await? {
return Err("Email taken");
}
self.repo.save(&user).await?;
self.email.send_welcome(&user).await?;
self.events.publish(UserRegistered::from(&user)).await?;
Ok(user)
}
}
问题:应用服务层成为副作用的集中地,业务流程仍然不清晰。
事件驱动:分散的复杂性
// 事件处理散落各处,执行流程不可见
impl EmailHandler {
async fn handle(&self, event: UserRegistered) -> Result<()> {
let user = self.repo.find(event.user_id).await?;
self.email.send_welcome(&user).await?;
self.events.publish(EmailSent { user_id: event.user_id }).await?;
Ok(())
}
}
问题:业务流程支离破碎,调试困难,整体逻辑难以把握。
三、Web 开发的本质矛盾
副作用:不可避免的核心
让我们面对现实:在 Web 开发中,副作用不是边缘功能,而是核心功能。一个典型的 Web 请求:
- 验证权限(读数据库)
- 处理业务(计算)
- 保存结果(写数据库)
- 通知用户(发消息)
- 记录日志(写日志)
纯函数只占很小一部分,大部分都是副作用!
混杂的代价
当业务逻辑与副作用混杂在一起时:
async fn transfer_money(&self, from: AccountId, to: AccountId, amount: Money) -> Result<()> {
// 这是业务规则还是技术实现?
let balance = self.db.get_balance(from).await?;
if balance < amount {
return Err("Insufficient funds");
}
// 副作用与业务逻辑交织
self.db.debit(from, amount).await?;
self.db.credit(to, amount).await?;
// 如果这里失败了怎么办?
self.notification.send_transfer_alert(from, to, amount).await?;
Ok(())
}
这种混杂最危险的不是代码难看,而是它隐藏了业务逻辑中的矛盾。
四、隐藏的系统杀手:未被发现的业务矛盾
为什么业务矛盾如此危险?
业务矛盾不同于普通的 bug:
- Bug:代码没有正确实现意图
- 业务矛盾:意图本身就是错误的
最可怕的是,这些矛盾往往隐藏在副作用的执行顺序中,直到生产环境才暴露。
一个真实的案例
// 看似正常的订单完成流程
async fn complete_order(&self, order_id: OrderId) -> Result<()> {
let order = self.db.get_order(order_id).await?;
// 扣减库存
for item in &order.items {
self.inventory.deduct(item.sku, item.quantity).await?;
}
// 扣款
self.payment.charge(order.user_id, order.total).await?;
// 发放积分
let points = order.total * 0.1;
self.points.grant(order.user_id, points).await?;
self.db.update_status(order_id, Status::Completed).await?;
Ok(())
}
隐藏的矛盾:
- 如果扣减库存后支付失败,库存怎么恢复?
- 如果订单退款,已发放的积分怎么处理?
- 如果积分服务宕机,订单应该失败还是继续? 这些问题在代码评审时很难发现,因为它们不是代码问题,而是业务逻辑问题。
传统架构为什么难以发现矛盾?
- 业务规则分散:散落在各个副作用调用中
- 隐式依赖:依赖关系通过执行顺序体现
- 测试困难:必须搭建完整环境才能测试
- 认知负担:需要在脑中模拟整个执行流程
五、EDD 的核心原理:让矛盾无处藏身
核心洞察:分离业务决策与副作用执行
EDD 的核心理念:业务逻辑负责决策,副作用负责执行。
// EDD 方式:业务决策清晰可见
pub async fn complete_order(
order_id: OrderId,
repo: &impl OrderRepository,
) -> Result<Outcome<CompletedOrder>> {
let order = repo.get(order_id).await?;
// 所有业务规则在此集中体现
if order.status != Status::Paid {
return Err(BusinessError::OrderNotPaid);
}
// 计算积分(纯业务逻辑)
let points = PointsCalculator::calculate(&order);
// 定义补偿策略(显式的业务决策)
let compensation = CompensationStrategy {
inventory: RestoreOnFailure,
points: Revocable(Duration::days(7)),
};
// 返回决策结果,而非立即执行
Ok(Outcome {
data: CompletedOrder { order, points, compensation },
from_case: AppUseCase::CompleteOrder,
events: vec![
AppEvent::DeductInventory {
items: order.items.clone(),
strategy: compensation.inventory
},
AppEvent::ChargePayment {
order_id,
amount: order.total,
},
AppEvent::GrantPoints {
user_id: order.user_id,
points,
revocable: true,
},
],
})
}
三个基本原理
- 副作用识别:在设计之初就识别所有副作用,分类管理,而非任其散落。
- 分层隔离原理:通过架构分层,将纯业务逻辑与副作用实现彻底隔离。
core/ → 纯业务逻辑(0副作用)
app/ → 业务编排(声明副作用)
event/ → 副作用协调
infra/ → 副作用实现
- 声明式编排原理:业务逻辑只声明"需要什么",不关心"如何执行"。
早期发现矛盾的机制
通过类型系统
// 强制思考补偿逻辑
pub enum EffectCompensation {
NoNeed, // 只读操作
Automatic(Strategy), // 可自动补偿
Manual(String), // 需人工介入
Impossible, // 无法补偿(如已发邮件)
}
impl AppEvent {
pub fn compensation(&self) -> EffectCompensation {
match self {
AppEvent::SendEmail { .. } => EffectCompensation::Impossible,
AppEvent::DeductInventory { .. } => EffectCompensation::Automatic(Restore),
_ => EffectCompensation::NoNeed,
}
}
}
通过测试验证
#[test]
fn test_order_completion_consistency() {
let order = create_test_order();
let outcome = complete_order(order.id, &mock_repo()).await.unwrap();
// 验证业务规则一致性
let has_payment = outcome.events.iter().any(|e| matches!(e, AppEvent::ChargePayment { .. }));
let has_points = outcome.events.iter().any(|e| matches!(e, AppEvent::GrantPoints { .. }));
// 业务规则:有积分必须先有支付
if has_points {
assert!(has_payment, "积分不能在支付前发放");
}
// 验证补偿策略一致性
for event in &outcome.events {
if let AppEvent::GrantPoints { revocable, .. } = event {
assert!(revocable, "订单可退款期内,积分必须可撤销");
}
}
}
六、EDD 的实践价值
1. 业务逻辑的可验证性
纯业务逻辑可以通过单元测试完整验证,无需搭建复杂环境:
#[test]
fn test_points_calculation() {
let order = Order {
total: Money::usd(100),
items: vec![/* ... */],
};
assert_eq!(PointsCalculator::calculate(&order), 100);
assert_eq!(PointsCalculator::calculate_vip(&order), 200);
}
2. 副作用的可控性
副作用通过事件机制统一管理:
- 可追踪:每个副作用都有明确的事件记录
- 可重试:失败的副作用可以独立重试
- 可监控:副作用执行情况一目了然
3. 系统的演进能力
新需求来临时:
- 增加副作用:只需添加新事件,不影响核心流程
- 修改业务规则:有完整测试保护,重构有信心
- 性能优化:可以独立优化业务计算或副作用执行
七、结语
EDD 不是银弹,但它提供了一种新的思考方式: 与其让副作用野蛮生长,不如给它们一个合适的位置。 通过承认副作用在 Web 开发中的核心地位,并为其设计专门的管理机制,EDD 实现了:
- 让隐藏的业务矛盾在设计阶段就暴露出来
- 让业务逻辑保持纯粹和可测试
- 让副作用变得可控和可追踪
记住 EDD 的核心理念: 副作用不可怕,不可控的副作用才可怕。 当我们能够在正确的时间(设计阶段)发现正确的问题(业务矛盾),我们就能构建更可靠的系统。 这就是 EDD 的哲学。