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

DDD实践:含可空ValueObject的实体如何验证值对象?

DDD实践疑问:Email有效性验证的职责边界

场景背景

我学习领域驱动设计(DDD)已有一段时间,编写代码时遇到了验证职责划分的问题。当前场景中定义了以下领域对象:

Email值对象

public class Email : IValueObject {
    private string _email = string.Empty;
    public string Value => _email;

    private Email(string email) {
        _email = email;
    }

    public static ErrorOr<Email> Create(string email) {
        var emailValue = new Email(email);

        var errors = Validate(emailValue);

        if (errors.Any()) {
            return errors;
        }

        return emailValue;
    }

    public static List<Error> Validate(Email email) {
        try {
            var emailValidator = new MailAddress(email.Value);
            return new List<Error>();
        }
        catch {
            return new List<Error>() { Common.Errors.Errors.Generic.InvalidEmail };
        }
    }
}

Agency实体

public sealed class Agency : StatefulBaseEntity, IAggregateRoot {
    private string _name;
    private Email _email;
    private Email? _pecEmail;

    public string Name => _name;
    public Email? PecEmail => _pecEmail;
    public Email Email => _email;

    public Agency(
        string name,
        Email? pecEmail,
        Email email,
        Guid id)  {
        _name = name;
        _pecEmail = pecEmail;
        _email = email;
        _id = id;
    }

    public static ErrorOr<Agency> Create(
        string name,
        Email? pecEmail,
        Email email,
        Guid? id = null) {

        var agency = new Agency(
            name,
            pecEmail,
            email,
            id ?? Guid.NewGuid());

        var errors = Validate(agency);

        if (errors.Any()) {
            return errors;
        }

        return agency;
    }


    private static List<Error> Validate(Agency agency) {
        var errors = new List<Error>();
        errors.AddRange(Email.Validate(agency.Email));
       
        if (agency.PecEmail is not null && !string.IsNullOrEmpty(agency.PecEmail.Value)) {
            errors.AddRange(Email.Validate(agency.PecEmail));
        }
        
        return errors;
    }
}

CreateAgency命令处理程序

internal sealed class CreateAgencyCommandHandler
    : IRequestHandler<CreateAgencyCommand, ErrorOr<AgencyResult>> {
    private readonly IAgencyRepository _agencyRepository;

    public CreateAgencyCommandHandler(IAgencyRepository agencyRepository) {
        _agencyRepository = agencyRepository;
    }

    public async Task<ErrorOr<AgencyResult>> Handle(
        CreateAgencyCommand request, CancellationToken cancellationToken) {
    
        var agency = Agency.Create(
            request.Name,
            Email.Create(request.PecEmail).Value,
            Email.Create(request.Email).Value);

        if (customer.IsError is not true) {
            _agencyRepository.Add(agency.Value);
            return new AgencyResult(agency.Value);
        }

        return await Task.FromResult(agency.Errors);
    }
}

Agency实体包含必填的Email属性和可选的PecEmail属性,当PecEmail不为空时必须保证其有效性。

核心疑问

Email的有效性验证应该放在Entity还是CommandHandler中?两种方案各有矛盾:

  • 在Entity中验证:虽然能让实体管控自身完整性,但理论上值对象只有有效实例才能存在,不该传入无效的Email对象,实体重复验证值对象显得冗余。
  • 在CommandHandler中验证:会导致创建、更新等多个命令处理程序重复相同的验证逻辑,违反DRY原则。

我更倾向于让Agency实体负责完整性验证,避免重复代码,想知道有没有更合理的实践方案。


实践建议

1. 坚守值对象的不变性原则

值对象的核心特性是不变性,即只有符合规则的实例才能被创建。你的Email.Create方法已经实现了格式验证,所以只要是通过该方法创建的实例,就一定是有效的——无效的输入会直接返回错误,不会生成Email对象。

基于这一点,Agency实体不需要再调用Email.Validate,因为传入的Email(包括PecEmail)要么是null,要么是经过验证的有效实例。

2. 拆分验证职责

  • 值对象层:负责自身的格式/结构验证(比如Email的格式合法性),确保只有有效实例能被实例化。
  • 实体层:负责自身的业务规则验证(比如Agency的Name不能为空、长度不能超过限制等),不重复值对象的验证逻辑。
  • 命令处理程序:负责将外部输入转换为领域对象,收集所有转换过程中的错误,确保只有有效的值对象进入实体。

3. 修正代码实现

修正后的CommandHandler

internal sealed class CreateAgencyCommandHandler
    : IRequestHandler<CreateAgencyCommand, ErrorOr<AgencyResult>> {
    private readonly IAgencyRepository _agencyRepository;
    private readonly IUnitOfWork _unitOfWork; // 假设存在工作单元

    public CreateAgencyCommandHandler(IAgencyRepository agencyRepository, IUnitOfWork unitOfWork) {
        _agencyRepository = agencyRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task<ErrorOr<AgencyResult>> Handle(
        CreateAgencyCommand request, CancellationToken cancellationToken) {
    
        // 先处理Email和PecEmail的创建,收集错误
        var emailResult = Email.Create(request.Email);
        var pecEmailResult = string.IsNullOrWhiteSpace(request.PecEmail) 
            ? ErrorOr<Email?>.Success(null) 
            : Email.Create(request.PecEmail);

        // 合并所有转换错误
        var errors = new List<Error>();
        if (emailResult.IsError) errors.AddRange(emailResult.Errors);
        if (pecEmailResult.IsError) errors.AddRange(pecEmailResult.Errors);
        
        if (errors.Any()) return errors;

        // 创建Agency,此时传入的都是有效的Email实例或null
        var agencyResult = Agency.Create(
            request.Name,
            pecEmailResult.Value,
            emailResult.Value);

        if (agencyResult.IsError) return agencyResult.Errors;

        _agencyRepository.Add(agencyResult.Value);
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return new AgencyResult(agencyResult.Value);
    }
}

修正后的Agency实体

public sealed class Agency : StatefulBaseEntity, IAggregateRoot {
    private string _name;
    private Email _email;
    private Email? _pecEmail;

    public string Name => _name;
    public Email? PecEmail => _pecEmail;
    public Email Email => _email;

    // 私有构造函数,确保只能通过Create方法创建
    private Agency(
        string name,
        Email? pecEmail,
        Email email,
        Guid id)  {
        _name = name;
        _pecEmail = pecEmail;
        _email = email;
        _id = id;
    }

    public static ErrorOr<Agency> Create(
        string name,
        Email? pecEmail,
        Email email,
        Guid? id = null) {

        var errors = new List<Error>();

        // 只验证Agency自身的业务规则
        if (string.IsNullOrWhiteSpace(name)) {
            errors.Add(Common.Errors.Errors.Agency.NameCannotBeEmpty);
        } else if (name.Length > 100) {
            errors.Add(Common.Errors.Errors.Agency.NameCannotExceed100Characters);
        }

        if (errors.Any()) return errors;

        return new Agency(
            name,
            pecEmail,
            email,
            id ?? Guid.NewGuid());
    }
}

4. 方案优势

  • 避免重复验证:Email的验证逻辑只在值对象层实现一次,所有依赖Email的地方都不需要重复编写。
  • 保证领域对象的有效性:实体只会接收有效的值对象,确保自身状态始终符合业务规则。
  • 职责清晰:各层只负责自己的职责,符合单一职责原则,代码维护性更强。

内容的提问来源于stack exchange,提问作者Gaetano Lenoci

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 14:05:22