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
相关产品推荐
相关产品推荐

