4、业务逻辑层:领域模型与状态、业务规则封装、用例驱动设计

好,我们进入第四讲。这一章,说实话,是整个分层架构里最考验设计功底的地方。前面我们讲了数据层和展示层,它们更多是「怎么存」和「怎么显」的问题。而业务逻辑层,要回答的是——「业务到底怎么跑」

我个人习惯把这一层称为「灵魂层」。为什么?因为代码里最能体现业务价值的地方,就在这里。你想想看,UI 可以换,数据库可以迁,但业务规则一旦写错,整个系统就废了。

4.1 领域模型与状态:别让业务「裸奔」

先聊领域模型。很多新手喜欢把数据对象(DTO)和领域模型混为一谈。我见过最夸张的项目,一个 Order 类里只有 getter/setter,所有业务逻辑散落在 Service 里。这其实就是在让业务「裸奔」。

领域模型应该是什么?它应该自带行为。举个例子:

// 反例:贫血模型
public class Order {
    private String status;
    private List<OrderItem> items;
    
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }
}

// 正例:充血模型
public class Order {
    private OrderStatus status;
    private List<OrderItem> items;
    
    public void submit() {
        if (items.isEmpty()) {
            throw new BusinessException("订单不能为空");
        }
        this.status = OrderStatus.SUBMITTED;
        // 触发领域事件
        DomainEventBus.publish(new OrderSubmittedEvent(this));
    }
    
    public void pay() {
        if (this.status != OrderStatus.SUBMITTED) {
            throw new BusinessException("只有已提交的订单才能支付");
        }
        this.status = OrderStatus.PAID;
    }
}

看到区别了吗?状态和行为是绑定的。我在项目中遇到过好几次,因为把状态判断逻辑写在 Service 里,导致后来业务规则一变,到处都要改。而把状态封装在模型里,改起来就一个地方。

核心原则:领域模型的状态迁移,必须由模型自身的方法来驱动。外部只能调用方法,不能直接修改状态。

这里有个避坑指南——我曾经接手过一个电商项目,订单状态用 int 表示:0=待支付,1=已支付,2=已发货。结果产品说「我们要加一个『部分退款』状态」,好嘛,所有 if-else 都要翻一遍。后来我重构时,果断用了枚举 + 状态机模式。

4.2 业务规则封装:别让规则「飘」在代码里

业务规则是什么?说白了就是「什么能做,什么不能做」。比如:

  • 「VIP 会员满 100 包邮」
  • 「同一手机号一天只能领一次优惠券」
  • 「订单金额超过 5000 需要经理审批」

这些规则如果散落在 Service 的各个方法里,那就是灾难。我建议的做法是——用规约模式(Specification Pattern)

// 规则封装
public class VipFreeShippingSpecification implements Specification<Order> {
    @Override
    public boolean isSatisfiedBy(Order order) {
        return order.getUser().isVip() 
            && order.getTotalAmount().compareTo(new BigDecimal("100")) >= 0;
    }
}

// 使用
public class ShippingService {
    private Specification<Order> freeShippingSpec;
    
    public BigDecimal calculateShipping(Order order) {
        if (freeShippingSpec.isSatisfiedBy(order)) {
            return BigDecimal.ZERO;
        }
        // 正常运费计算...
    }
}

你想想看,这样做的好处是什么?规则可以组合、可以复用、可以单独测试。我有个项目,光优惠规则就有 20 多种,用规约模式后,新增一种规则只需要加一个类,完全不影响现有逻辑。

我的个人经验:业务规则一定要写单元测试。别偷懒。我曾经因为一个「满减规则」的边界条件没测到,上线后导致用户多付了钱,被运营追着骂了一周。

4.3 用例驱动设计:从「功能」到「业务价值」

最后聊用例驱动。这个思想来自 Alistair Cockburn 的《编写有效用例》。说白了就是——别从技术角度想「我要提供什么接口」,而是从用户角度想「用户要完成什么目标」

举个例子。一个「用户下单」的功能,如果按传统 MVC 思路,你会写:

  • OrderController.createOrder()
  • OrderService.createOrder()
  • OrderDao.insert()

但按用例驱动,你应该先定义用例:

用例名称 主流程 扩展流程
用户提交订单 1. 用户选择商品
2. 系统校验库存
3. 系统计算价格
4. 用户确认支付
2a. 库存不足 → 提示用户
3a. 优惠券过期 → 重新计算
用户取消订单 1. 用户发起取消
2. 系统校验状态
3. 系统释放库存
4. 系统发起退款
2a. 订单已发货 → 拒绝取消

然后,每个用例对应一个用例类

public class SubmitOrderUseCase {
    private final OrderRepository orderRepo;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;
    
    public SubmitOrderResult execute(SubmitOrderCommand command) {
        // 1. 校验库存
        if (!inventoryService.checkStock(command.getItems())) {
            return SubmitOrderResult.failure("库存不足");
        }
        
        // 2. 创建订单(领域模型)
        Order order = Order.create(command.getUserId(), command.getItems());
        
        // 3. 保存订单
        orderRepo.save(order);
        
        // 4. 发起支付
        paymentService.initiatePayment(order);
        
        return SubmitOrderResult.success(order.getId());
    }
}

这样做的好处很明显:每个用例就是一个独立的业务场景。测试时,你只需要 mock 依赖,然后验证这个用例的输入输出。我现在的项目里,每个用例类都对应一个测试类,覆盖率轻松上 90%。

注意:用例类不要写太大。如果一个用例超过 200 行,说明它可能包含了多个子用例。我见过有人把「用户注册 + 发送欢迎邮件 + 初始化默认配置」写在一个用例里,结果改邮件逻辑时,不小心影响了注册流程。拆!

4.4 三者如何协同?

嗯,到这里你可能要问:领域模型、业务规则、用例,它们怎么配合?

我画个简单的协作图给你看:

  • 用例层:接收外部请求,编排流程。它知道「先做什么,后做什么」。
  • 领域模型:执行业务操作。它知道「怎么做」。
  • 业务规则:作为守卫。它知道「能不能做」。

举个例子,用户提交订单时:

  1. 用例层调用 SubmitOrderUseCase.execute()
  2. 用例层先调用 VipFreeShippingSpecification 判断是否包邮
  3. 然后调用 Order.submit(),模型内部校验状态并触发事件
  4. 最后用例层调用 orderRepo.save() 持久化

你看,各司其职。改规则时只改 Specification,改流程时只改 UseCase,改状态逻辑时只改 Model。互不干扰。

总结一句话:领域模型管状态,业务规则管约束,用例管流程。这三者分清楚了,你的业务逻辑层就稳了。

好,这一章就到这里。下一章我们聊聊「服务层与基础设施层的解耦」,到时候我会分享一个我踩过的坑——关于事务边界怎么切,才能既保证一致性又不影响性能。敬请期待。