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 三者如何协同?
嗯,到这里你可能要问:领域模型、业务规则、用例,它们怎么配合?
我画个简单的协作图给你看:
- 用例层:接收外部请求,编排流程。它知道「先做什么,后做什么」。
- 领域模型:执行业务操作。它知道「怎么做」。
- 业务规则:作为守卫。它知道「能不能做」。
举个例子,用户提交订单时:
- 用例层调用
SubmitOrderUseCase.execute() - 用例层先调用
VipFreeShippingSpecification判断是否包邮 - 然后调用
Order.submit(),模型内部校验状态并触发事件 - 最后用例层调用
orderRepo.save()持久化
你看,各司其职。改规则时只改 Specification,改流程时只改 UseCase,改状态逻辑时只改 Model。互不干扰。
总结一句话:领域模型管状态,业务规则管约束,用例管流程。这三者分清楚了,你的业务逻辑层就稳了。
好,这一章就到这里。下一章我们聊聊「服务层与基础设施层的解耦」,到时候我会分享一个我踩过的坑——关于事务边界怎么切,才能既保证一致性又不影响性能。敬请期待。