一、当建模变成炫技表演
有个现象特别有意思:很多团队刚开始接触领域驱动设计时,会把建模会议开成UML绘图大赛。我见过最夸张的案例是,某电商系统在商品模块设计了17个继承层次,包含"抽象商品基类"、"虚拟商品接口"、"可退货商品装饰器"等华丽结构。结果上线后发现,他们80%的业务场景只需要商品名称和价格两个字段。
// 过度设计的商品类示例(Java技术栈)
public abstract class AbstractProduct {
private Long id;
private String name;
// 十余个抽象方法声明...
}
public interface DigitalProduct {
void validateLicense();
// 其他数字商品特有方法
}
public class RefundableProductDecorator extends Product {
// 装饰器模式实现
// 实际业务中退款逻辑其实就两行代码
}
这种过度建模就像给自行车装航天发动机,不仅浪费开发资源,更可怕的是会让业务方产生"这个系统很复杂"的认知负担。我曾参与改造过一个保险系统,他们把简单的保单状态流转做成了状态模式+策略模式的组合,结果每次业务规则调整都要修改5个类。
二、业务语义的失真表达
更隐蔽的问题是模型偏离业务语言。某银行项目里,业务人员说的"账户冻结",在代码里变成了"AccountStatusManagementService.changeState()"。这种术语断层会导致两个严重后果:
- 新成员需要做业务-代码的术语映射
- 业务规则变更时找不到对应代码位置
// 不良实践:技术术语与业务脱节
public class AccountService {
public void updateAccountFlag(Long accountId, int flagCode) {
// 方法参数用魔法数字表示业务状态
}
}
// 改进后的领域模型
public class BankAccount {
public void freeze(FreezeReason reason) {
// 使用业务术语作为方法签名
}
}
在物流系统项目中,我们通过事件风暴工作坊发现,业务人员说的"转运"包含三个明确阶段:分拣、干线运输、末端配送。但原有代码却把这些都塞进一个TransportService里。通过建立Sorter、LineHaul、LastMile等领域对象,不仅代码更清晰,还意外发现了业务流程中的优化点。
三、贫血模型的诱惑陷阱
很多团队会不自觉陷入贫血模型陷阱,比如这个订单处理的典型例子:
// 贫血模型示例(Java+Spring技术栈)
public class Order {
// 只有属性和getter/setter
private Long id;
private String status;
// ...其他字段
}
@Service
public class OrderService {
@Transactional
public void approveOrder(Long orderId) {
Order order = orderRepository.findById(orderId);
if ("CREATED".equals(order.getStatus())) {
order.setStatus("APPROVED");
// 数十行业务逻辑
}
}
}
这种写法的问题在于:
- 业务规则分散在服务层
- 领域对象没有行为能力
- 状态变更缺乏保护
我们改造后采用富领域模型:
public class Order {
private OrderStatus status;
public void approve() {
if (!status.canApprove()) {
throw new IllegalStateException();
}
this.status = OrderStatus.APPROVED;
this.approvedAt = LocalDateTime.now();
}
}
在电商秒杀系统实践中,将库存扣减逻辑从Service移到Product聚合根内部,不仅解决了并发问题,还使业务规则变更更可控。比如后来增加的区域库存限制,只需要修改Product类而不影响其他模块。
四、测试驱动的建模验证
如何判断模型是否合理?我的经验是用测试案例验证。某次金融项目中,我们通过测试用例发现了模型缺陷:
// 测试用例暴露模型问题(Java+JUnit5)
@Test
void should_reject_invalid_transfer() {
Account from = new Account(1000);
Account to = new Account(100);
Money amount = new Money(2000);
assertThrows(InsufficientBalanceException.class,
() -> from.transferTo(to, amount));
}
// 最初实现漏了资金冻结场景
@Test
void should_block_frozen_account() {
Account account = new Account(1000);
account.freeze();
assertThrows(AccountFrozenException.class,
() -> account.withdraw(500));
}
在医疗系统开发时,我们通过编写"患者出院时未结算禁止办理"的测试用例,发现了结算逻辑应该放在Patient聚合根而不是单独的BillingService中。这种测试优先的方法能有效避免过度工程。
五、持续演进的关键实践
好的领域模型需要持续打磨。在某SaaS平台项目中,我们建立了这些机制:
- 每月领域模型评审会,邀请业务方参与
- 代码中的@DomainStory注解标记业务背景
- 版本发布时同步更新领域词汇表
/**
* @DomainStory
* 客户投诉处理流程的核心聚合
* 业务规则:
* - 紧急投诉需30分钟内响应
* - 重复投诉自动升级
*/
public class Complaint {
private List<FollowUp> histories;
public void escalateIfNeeded() {
// 业务规则实现
}
}
物流跟踪系统通过事件溯源(Event Sourcing)技术,将业务人员的"包裹已到达分拣中心"这类陈述直接转化为领域事件,使模型始终保持与业务语言同步。
六、价值衡量的实用准则
最后分享几个判断模型价值的实用方法:
- 5分钟测试:能否在5分钟内给新人讲清楚核心领域?
- 变更成本评估:添加新业务规则要改多少层代码?
- 业务验证:模型是否解决了真实的业务痛点?
在零售库存系统改造中,我们通过将"安全库存"、"在途库存"等业务概念显式建模,使原本需要2周完成的月度报表开发缩短到3天。这才是领域驱动设计真正的价值体现。
记住,好的领域模型应该像精准的导航仪,而不是华丽的汽车模型。它不需要展示所有技术细节,但要确保团队不会在业务复杂度中迷路。当你在白板前画框图时,不妨问问:这个设计会让业务人员点头还是皱眉?这个问题的答案往往就是建模是否合理的试金石。
评论