一、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个类似字段...
}

这种设计看似"规范",但实际上:

  1. 简单的用户注册功能需要新建10个类
  2. 每次修改字段都要穿透多层验证
  3. 新同事接手时要理解半小时才能改一行代码

二、实用主义建模四原则

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;
        }
    }
}

四、落地检查清单

  1. 必要性验证

    • 这个抽象会被多处复用吗?
    • 没有它会导致代码混乱吗?
  2. 可读性测试
    让团队新人阅读代码:

    • 能否在30分钟内理解核心流程?
    • 能否独立完成功能扩展?
  3. 修改成本评估

    • 增加新字段需要改几个文件?
    • 业务规则变更是否需要大面积重构?
  4. 技术债务监控
    当出现以下信号时需要警惕:

    • 简单的CRUD操作需要涉及10个类
    • 50%的模型类只有1个使用场景
    • 团队开始抱怨"改不动代码"

记住:好的DDD实现应该像一份城市地图——

  • 主干道清晰明确(核心领域)
  • 小巷子灵活可变(非核心领域)
  • 随时可以扩建新区域(扩展性)
  • 游客也能找到路(可维护性)