You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

命令中能否使用值对象?值对象验证逻辑位置指导请求

解决方案:值对象验证时机与命令/事件处理的协同

这是DDD结合事件溯源时非常常见的痛点——既要复用值对象的验证逻辑,又要保证验证时机落在命令处理阶段(而不是事件重放时)。下面给你两个实用的方案,结合你的代码示例来讲解:


方案1:拆分值对象的验证逻辑,实现复用

核心思路是把Address的验证逻辑从构造函数中抽离成独立的静态方法,这样命令处理器可以提前调用验证,而事件溯源handler创建值对象时依然能复用相同的逻辑(作为兜底)。

修改Address类,抽离验证逻辑

public class Address {
    private String city;
    private String country;
    private String zipCode;

    // 私有构造函数,强制通过工厂方法创建
    private Address(String city, String country, String zipCode) {
        this.city = city;
        this.country = country;
        this.zipCode = zipCode;
    }

    // 静态工厂方法,内部调用验证逻辑
    public static Address create(String city, String country, String zipCode) {
        validateAddress(city, country, zipCode);
        return new Address(city, country, zipCode);
    }

    // 独立的公开验证方法,供命令处理器调用
    public static void validateAddress(String city, String country, String zipCode) {
        if (city == null || city.isBlank()) {
            throw new IllegalArgumentException("City cannot be empty or null");
        }
        if (country == null || country.isBlank()) {
            throw new IllegalArgumentException("Country cannot be empty or null");
        }
        if (zipCode == null || zipCode.isBlank()) {
            throw new IllegalArgumentException("Zip code cannot be empty or null");
        }
        // 其他自定义验证规则(比如邮编格式匹配)
    }

    // Getters...
}

命令处理器中提前执行验证

在CreateShopCmd的处理器里,先调用Address的验证方法,确保无效请求在发布事件前被拦截:

@CommandHandler
public Shop(CreateShopCmd cmd) {
    // 1. 先验证地址信息
    Address.validateAddress(cmd.getCity(), cmd.getCountry(), cmd.getZipCode());
    
    // 2. 验证其他命令字段(比如店铺名称)
    if (cmd.getShopName() == null || cmd.getShopName().isBlank()) {
        throw new IllegalArgumentException("Shop name cannot be empty");
    }

    // 3. 发布已验证的事件
    ShopRegistredEvt shopRegistredEvt = new ShopRegistredEvt();
    shopRegistredEvt.setShopId(cmd.getShopId());
    shopRegistredEvt.setShopName(cmd.getShopName());
    shopRegistredEvt.setCity(cmd.getCity());
    shopRegistredEvt.setCountry(cmd.getCountry());
    shopRegistredEvt.setZipCode(cmd.getZipCode());
    
    AggregateLifecycle.apply(shopRegistredEvt);
}

事件溯源handler中使用工厂方法创建值对象

事件是已经确认的领域事实,这里的验证更多是兜底(防止事件被意外篡改),但因为命令阶段已经验证过,不会触发异常:

@EventSourcingHandler
public void on(ShopRegistredEvt evt) {
    this.shopName = evt.getShopName();
    // 用工厂方法创建Address,复用验证逻辑
    this.address = Address.create(evt.getCity(), evt.getCountry(), evt.getZipCode());
}

方案2:命令处理器中直接创建值对象,验证时机前置

这种方案更符合DDD的实践:值对象的验证逻辑由自身负责,命令处理器通过创建值对象的方式触发验证,事件中直接携带已验证的值对象(或其原始字段)。

命令处理器中创建Address值对象

@CommandHandler
public Shop(CreateShopCmd cmd) {
    try {
        // 创建Address时直接触发构造函数中的验证
        Address address = new Address(cmd.getCity(), cmd.getCountry(), cmd.getZipCode());
        
        // 验证其他命令字段
        if (cmd.getShopName() == null || cmd.getShopName().isBlank()) {
            throw new IllegalArgumentException("Shop name cannot be empty");
        }

        // 发布事件,事件可以携带Address对象(如果支持序列化)或拆分字段
        ShopRegistredEvt shopRegistredEvt = new ShopRegistredEvt();
        shopRegistredEvt.setShopId(cmd.getShopId());
        shopRegistredEvt.setShopName(cmd.getShopName());
        // 方式A:直接传递值对象(需要确保事件序列化支持值对象)
        shopRegistredEvt.setAddress(address);
        // 方式B:传递拆分后的字段(如果需要更灵活的存储/查询)
        // shopRegistredEvt.setCity(cmd.getCity());
        // shopRegistredEvt.setCountry(cmd.getCountry());
        // shopRegistredEvt.setZipCode(cmd.getZipCode());
        
        AggregateLifecycle.apply(shopRegistredEvt);
    } catch (IllegalArgumentException e) {
        // 把验证异常包装成命令执行异常,返回给调用方
        throw new CommandExecutionException("Failed to create shop: " + e.getMessage(), e);
    }
}

事件溯源handler中直接赋值

因为事件中的数据已经是命令阶段验证过的,这里不需要再执行验证:

@EventSourcingHandler
public void on(ShopRegistredEvt evt) {
    this.shopName = evt.getShopName();
    // 如果事件携带值对象,直接赋值
    this.address = evt.getAddress();
    // 如果事件携带拆分字段,直接创建(此时验证是兜底,但不会失败)
    // this.address = new Address(evt.getCity(), evt.getCountry(), evt.getZipCode());
}

关键注意点

  1. 验证必须在命令处理阶段完成:事件是已经发生的事实,不应该在事件重放(事件溯源handler)时进行验证——否则如果后续修改了验证规则,旧事件重放会失败,破坏事件溯源的可靠性。
  2. 值对象的验证逻辑应该内聚:尽量让值对象自己负责自身的合法性,避免在外部重复编写验证代码,符合单一职责原则。
  3. 事件的可信性:事件中的数据应该是经过验证的干净数据,不要把未验证的原始数据放进事件里。

内容的提问来源于stack exchange,提问作者Ashwani Tiwari

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 07:39:13