一、DDD建模的初心:解决问题而非制造问题
刚开始接触DDD(领域驱动设计)时,很多人容易陷入一个误区:把模型设计得越"纯粹"越好,恨不得每个对象都严格遵循"单一职责"。但实际开发中,这种追求往往会导致代码复杂度飙升,甚至让团队陷入"过度设计"的泥潭。
举个例子(技术栈:Java + Spring Boot):
// 反例:过度设计的"用户模型"
public class User {
private UserId id; // 专门的值对象
private UserName name; // 专门的值对象
private UserEmail email; // 专门的值对象
// 嵌套了3层的数据校验逻辑
public static class UserEmail {
private String value;
public UserEmail(String value) {
if (!Pattern.matches("正则表达式", value)) {
throw new IllegalArgumentException("邮箱格式错误");
}
this.value = value;
}
}
// 其他20个类似字段...
}
这种设计看似"规范",但实际上:
- 简单的用户注册功能需要新建10个类
- 每次修改字段都要穿透多层验证
- 新同事接手时要理解半小时才能改一行代码
二、实用主义建模四原则
1. 80分原则
模型能满足当前需求即可,不必预测未来所有可能性。比如电商系统的"订单"模型:
// 正例:够用且易维护的订单模型
public class Order {
private String orderId; // 直接使用String而非OrderId值对象
private List<Product> items;
private BigDecimal total; // 直接用BigDecimal而非Money类型
private Address address; // 只有复杂结构才单独建模
// 核心业务逻辑仍保持清晰
public void applyDiscount(Coupon coupon) {
this.total = coupon.calculateDiscount(this.total);
}
}
2. 上下文隔离
不同业务场景用不同模型。例如同一个"用户"概念:
- 注册场景:只需要邮箱和密码
- 支付场景:需要绑定的银行卡信息
- 社交功能:需要关注关系和动态
// 示例:按场景拆分的用户模型
public class RegistrationUser {
private String email;
private String password;
}
public class PaymentUser {
private String userId;
private List<BankCard> cards;
}
3. 动态纯度控制
根据业务发展阶段调整模型严格度:
// 初创期快速迭代版本
public class QuickProduct {
private String id;
private String name;
private double price;
}
// 成熟期严格版本
public class StrictProduct {
private ProductId id; // 值对象
private ProductName name; // 值对象
private ProductPrice price; // 包含货币单位校验
}
4. 团队共识优于理论完美
在代码评审时应该关注:
- 新模型是否让现有功能更难修改?
- 额外抽象是否被3个以上场景复用?
- 团队新人能否在1小时内理解?
三、典型场景的平衡案例
案例1:库存管理系统
错误做法:为库存设计状态模式、策略模式、领域事件等全套架构
// 过度设计的库存模型
public class Inventory {
private InventoryState state; // 状态模式
private StockPolicy policy; // 策略模式
private List<DomainEvent> events; // 事件溯源
}
正确做法:
// 满足当前需求的库存模型
public class Inventory {
private String sku;
private int stock;
private int locked; // 预占库存
// 核心方法保持领域语义
public void deduct(int quantity) {
if (this.stock - this.locked < quantity) {
throw new RuntimeException("库存不足");
}
this.stock -= quantity;
}
}
案例2:物流轨迹跟踪
必要复杂度的合理设计:
// 物流轨迹的恰当抽象
public class ShippingTrace {
private List<Location> history; // 值对象合理
public static class Location {
private final double lat; // 基本类型即可
private final double lng;
private String city; // 必要语义
public Location(double lat, double lng, String city) {
// 只做必要校验
if (city == null) throw new IllegalArgumentException();
this.lat = lat;
this.lng = lng;
this.city = city;
}
}
}
四、落地检查清单
必要性验证
- 这个抽象会被多处复用吗?
- 没有它会导致代码混乱吗?
可读性测试
让团队新人阅读代码:- 能否在30分钟内理解核心流程?
- 能否独立完成功能扩展?
修改成本评估
- 增加新字段需要改几个文件?
- 业务规则变更是否需要大面积重构?
技术债务监控
当出现以下信号时需要警惕:- 简单的CRUD操作需要涉及10个类
- 50%的模型类只有1个使用场景
- 团队开始抱怨"改不动代码"
记住:好的DDD实现应该像一份城市地图——
- 主干道清晰明确(核心领域)
- 小巷子灵活可变(非核心领域)
- 随时可以扩建新区域(扩展性)
- 游客也能找到路(可维护性)
评论