跳至主要內容

EDD 副作用驱动开发

Mr.Lexon大约 7 分钟EDD

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 请求:

  1. 验证权限(读数据库)
  2. 处理业务(计算)
  3. 保存结果(写数据库)
  4. 通知用户(发消息)
  5. 记录日志(写日志)

纯函数只占很小一部分,大部分都是副作用!

混杂的代价

当业务逻辑与副作用混杂在一起时:

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(())
}

隐藏的矛盾

  1. 如果扣减库存后支付失败,库存怎么恢复?
  2. 如果订单退款,已发放的积分怎么处理?
  3. 如果积分服务宕机,订单应该失败还是继续? 这些问题在代码评审时很难发现,因为它们不是代码问题,而是业务逻辑问题

传统架构为什么难以发现矛盾?

  1. 业务规则分散:散落在各个副作用调用中
  2. 隐式依赖:依赖关系通过执行顺序体现
  3. 测试困难:必须搭建完整环境才能测试
  4. 认知负担:需要在脑中模拟整个执行流程

五、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,
            },
        ],
    })
}

三个基本原理

  1. 副作用识别:在设计之初就识别所有副作用,分类管理,而非任其散落。
  2. 分层隔离原理:通过架构分层,将纯业务逻辑与副作用实现彻底隔离。
core/   → 纯业务逻辑(0副作用)
app/    → 业务编排(声明副作用)
event/  → 副作用协调
infra/  → 副作用实现
  1. 声明式编排原理:业务逻辑只声明"需要什么",不关心"如何执行"。

早期发现矛盾的机制

通过类型系统

// 强制思考补偿逻辑
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 实现了:

  1. 让隐藏的业务矛盾在设计阶段就暴露出来
  2. 让业务逻辑保持纯粹和可测试
  3. 让副作用变得可控和可追踪

记住 EDD 的核心理念: 副作用不可怕,不可控的副作用才可怕。 当我们能够在正确的时间(设计阶段)发现正确的问题(业务矛盾),我们就能构建更可靠的系统。 这就是 EDD 的哲学。

上次编辑于:
贡献者: lexon