领域驱动设计中的验证

Posted

技术标签:

【中文标题】领域驱动设计中的验证【英文标题】:Validation in a Domain Driven Design 【发布时间】:2010-10-05 17:19:04 【问题描述】:

您如何处理域驱动设计中复杂聚合的验证?您是否正在整合您的业务规则/验证逻辑?

我了解参数验证,我了解可以附加到模型本身的属性验证,并执行诸如检查电子邮件地址或邮政编码是否有效或名字具有最小和最大长度之类的事情。

但是涉及多个模型的复杂验证呢?您通常将这些规则和方法放在您的架构中的什么位置?如果有的话,你用什么模式来实现它们?

【问题讨论】:

【参考方案1】:

不要在整个应用程序中依赖 IsValid(xx) 调用,而是考虑从 Greg Young 那里获得一些建议:

永远不要让你的实体进入 无效状态。

这基本上意味着您不再将实体视为纯数据容器,而更多地考虑具有行为的对象。

考虑一个人的地址的例子:

 person.Address = "123 my street";
 person.City = "Houston";
 person.State = "TX";
 person.Zip = 12345;

在任何这些调用之间,您的实体都是无效的(因为您的属性彼此不一致。现在考虑一下:

person.ChangeAddress(.......); 

所有与更改地址行为相关的调用现在都是一个原子单元。您的实体在这里永远是无效的。

如果您采用这种建模行为而不是状态的想法,那么您可以获得一个不允许无效实体的模型。

如需对此进行深入讨论,请查看此 infoq 采访:http://www.infoq.com/interviews/greg-young-ddd

【讨论】:

我一直认为您发布的 Greg Young 的建议在现实世界中根本不实用。 他不只是因为它听起来不错而提倡它,他在这种思维方式中拥有非常大的系统。 想象有一个对象封装了一个 List 事物 - Things。在处理的下一个阶段,在它被填充之后,我必须确保列表具有一定的元素,并且它是它的唯一元素。我怎样才能让Things 不进入无效状态?作为一种可能的解决方案,我认为要求用户提供此对象作为列表的第一个元素,但我不能这样做。所以对象Things 可以处于它的转换状态,它可以一直保持到下一个阶段的处理开始。这就是为什么我必须在处理之前对其进行验证。 我也倾向于认为很多业务逻辑只是验证(即在我们改变一些状态之前,可以做这个操作吗?)。 DDD 适用于复杂领域。有时复杂性只是对该域的复杂验证。你把它拿出来,你就有了一个贫乏的领域实体,你将不得不在别处寻找规则。如果实体变得太大,也许稍后取消一些规则是合理的。但这可以在以后完成(以避免过早的过度优化)。或者也许考虑改造聚合体。 规范似乎更多地满足实体在其自己的领域中具有但无法满足的需求。实体可以只提供一个规范,而不是引入对外部世界的聚合的双向访问。域服务可以接受并实现它(可能进行存储库调用)并返回类似值对象的东西。然后可以将 VO 传递到实体上的构造/方法中以满足所需的需求。【参考方案2】:

我喜欢 Jimmy Bogard 对这个问题的解决方案。他在他的博客上有一篇名为 "Entity validation with visitors and extension methods" 的帖子,其中他提出了一种非常优雅的实体验证方法,建议实现一个单独的类来存储验证代码。

public interface IValidator<T>

    bool IsValid(T entity);
    IEnumerable<string> BrokenRules(T entity);


public class OrderPersistenceValidator : IValidator<Order>

    public bool IsValid(Order entity)
    
        return BrokenRules(entity).Count() == 0;
    

    public IEnumerable<string> BrokenRules(Order entity)
    
        if (entity.Id < 0)
            yield return "Id cannot be less than 0.";

        if (string.IsNullOrEmpty(entity.Customer))
            yield return "Must include a customer.";

        yield break;
    

【讨论】:

Bogard 文章中描述的方法看起来很方便。谢谢。 有一点,return BrokenRules(entity).Count() &gt; 0 不应该是 return BrokenRules(entity).Count() &lt;= 0 吗? 我刚刚看了 Jimmy 的帖子,基本上在本地进行了快速测试,我想说我不喜欢这种方法的一件事是,如果你使用他的文章中的扩展方法来实现它,当消费者键入“实体”时,会出现两个 Validate() 方法。一种是坐在实体上实现IValidatable接口,另一种是扩展方法。我是否正在使用具有两个 .Validate() 方法的实体,如果不查看每个方法,我可能不知道要调用哪个。 对于 entity.Customer,我们必须创建 CustomerValidator? 这不是违反域实体必须始终有效的规则吗?【参考方案3】:

我通常使用规范类, 它提供了一种方法(这是 C#,但您可以将其翻译成任何语言):

bool IsVerifiedBy(TEntity candidate)

此方法对候选人及其关系进行全面检查。 您可以在规范类中使用参数来使其参数化,例如检查级别...

您还可以添加一个方法来了解候选人未验证规范的原因:

IEnumerable<string> BrokenRules(TEntity canditate) 

您可以简单地决定像这样实现第一个方法:

bool IsVerifiedBy(TEntity candidate)

  return BrokenRules(candidate).IsEmpty();

对于违反规则,我通常会写一个迭代器:

IEnumerable<string> BrokenRules(TEntity candidate)

  if (someComplexCondition)
      yield return "Message describing cleary what is wrong...";
  if (someOtherCondition) 
      yield return
   string.Format("The amount should not be 0 when the state is 1",
        amount, state);

对于本地化,您应该使用资源,为什么不将文化传递给 BrokenRules 方法。 我将这些类放在模型命名空间中,其名称表明它们的用途。

【讨论】:

【参考方案4】:

多个模型验证应该通过您的聚合根。如果您必须跨聚合根进行验证,则可能存在设计缺陷。

我对聚合进行验证的方式是返回一个响应接口,告诉我验证是否通过/失败以及有关失败原因的任何消息。

您可以验证聚合根上的所有子模型,以便它们保持一致。

// Command Response class to return from public methods that change your model
public interface ICommandResponse

    CommandResult Result  get; 
    IEnumerable<string> Messages  get; 


// The result options
public enum CommandResult

    Success = 0,
    Fail = 1


// My default implementation
public class CommandResponse : ICommandResponse

    public CommandResponse(CommandResult result)
    
        Result = result;
    

    public CommandResponse(CommandResult result, params string[] messages) : this(result)
    
        Messages = messages;
    

    public CommandResponse(CommandResult result, IEnumerable<string> messages) : this(result)
    
        Messages = messages;
    

    public CommandResult Result  get; private set; 

    public IEnumerable<string> Messages  get; private set; 


// usage
public class SomeAggregateRoot

    public string SomeProperty  get; private set; 


    public ICommandResponse ChangeSomeProperty(string newProperty)
    
        if(newProperty == null)
        
            return new CommandResponse(CommandResult.Fail, "Some property cannot be changed to null");
        

        SomeProperty = newProperty;

        return new CommandResponse(CommandResult.Success);
    

【讨论】:

【参考方案5】:

这个问题现在有点老了,但如果有人感兴趣,这里是我如何在我的服务类中实现验证。

我在每个服务类中都有一个私有 Validate 方法,该方法接受一个实体实例和正在执行的操作,如果验证失败,则会引发自定义异常,其中包含违反规则的详细信息。

带有内置验证的示例 DocumentService

public class DocumentService : IDocumentService

    private IRepository<Document> _documentRepository;

    public DocumentService(IRepository<Document> documentRepository)
    
        _documentRepository = documentRepository;
    

    public void Create(Document document)
    
        Validate(document, Action.Create);

        document.CreatedDate = DateTime.Now;

        _documentRepository.Create(document);
    

    public void Update(Document document)
    
        Validate(document, Action.Update);

        _documentRepository.Update(document);
    

    public void Delete(int id)
    
        Validate(_documentRepository.GetById(id), Action.Delete);

        _documentRepository.Delete(id);
    

    public IList<Document> GetAll()
    
        return _documentRepository
            .GetAll()
            .OrderByDescending(x => x.PublishDate)
            .ToList();
    

    public int GetAllCount()
    
        return _documentRepository
            .GetAll()
            .Count();
    

    public Document GetById(int id)
    
        return _documentRepository.GetById(id);
    

    // validation 

    private void Validate(Document document, Action action)
    
        var brokenRules = new List<string>();

        if (action == Action.Create || action == Action.Update)
        
            if (string.IsNullOrWhiteSpace(document.Title))
                brokenRules.Add("Title is required");

            if (document.PublishDate == null)
                brokenRules.Add("Publish Date is required");
        

        if (brokenRules.Any())
            throw new EntityException(string.Join("\r\n", brokenRules));
    

    private enum Action
    
        Create,
        Update,
        Delete
    

我喜欢这种方法,因为它让我可以将所有核心验证逻辑放在一个地方,让事情变得简单。

【讨论】:

以上是关于领域驱动设计中的验证的主要内容,如果未能解决你的问题,请参考以下文章

《领域驱动设计》读书笔记

领域驱动设计:软件核心复杂性应对之道pdf

领域驱动设计-什么是领域驱动设计和怎么使用它

演进架构中的领域驱动设计

学习:DDD领域驱动设计

领域驱动设计:服务和聚合中的领域规则