不抛出异常的 DDD 验证

Ste*_*kes 7 c# validation domain-driven-design

我正在尝试第一次涉足 DDD,我在这里问了一个关于批量导入的问题,但我正在绕圈子试图对我的域模型应用验证。

本质上,我想在不抛出异常的情况下运行所有​​验证,以便我可以通过 Command 对象内的 CommandResult 对象列表拒绝带有所有验证错误的命令。虽然有些只是可配置的强制性字段检查,因此将在聚合之外处理,但也有业务规则,因此我不想复制验证逻辑,也不想通过移动所有内容而陷入贫血模型在聚合之外保持实体的始终有效的口头禅。

我有点不知所措,所以我认为最好在我开始进一步混淆水域之前询问专家我是否正确处理事情!

尝试和演示:

以下面为例,我们有相当简单的 UserProfile 聚合,构造函数获取配置文件存在所需的最少信息。

 public class UserProfile : AggregateRoot
    {
        public Guid Id {get; private set; }
        public Name Name {get private set;}
        public CardDetail PaymentInformation {get; private set;}



      public UserProfile(Guid id, Name name, CardDetail paymentInformation)
        {
            Name = name;
            PaymentInformation = paymentInformation;
        }

    }

public class CardDetail : ValueObject
{
    public string Number {get; private set;}
    public string CVC {get; private set; }
    public DateTime? IssueDate {get; private set;}
    public DateTime ExpiryDate {get;private set;}

    public CardDetail(string number, string cvc, DateTime? issueDate, DateTime expiryDate)
    {
        if(!IsValidCardNumber(number))
        {
            /*Do something to say details invalid, but not throw exception, possibly?*/
        }
        Number = number;
        CVC = cvc;
        IssueDate = issueDate



        ExpiryDate = expiryDate;

    }

    private bool IsValidCardNumber(string number)
    {
        return Regex.IsMatch(/*regex for card number*/);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我有一个接受命令对象的方法,它将构造一个 UserProfile 并保存到数据库中,但我想在保存之前进行验证

public void CreateProfile(CreateProfileCommand command)
{
    var paymentInformation = new CardDetail(command.CardNumber, command.CardCVC, command.CardIssueDate, command.CardExpiryDate)

    var errors = /* list of errors added to from card detail validation, possibly? */

    var profile = new UserProfile(/* pass args, add to errors? */

    if(errors.Any())
    {
        command.Results.Add(errors.Select(x => new CommandResult { Severity = Severity.Error, Message = x.Message });
        return;
    }

    /* no errors, so continue to save */

}
Run Code Online (Sandbox Code Playgroud)

现在,我可以处理异常并将它们添加到命令结果中,但这似乎很昂贵并且肯定违反了允许异常控制流的规则?但另一方面,我想保持实体和值对象有效,所以我发现自己有点老套!

此外,在上面的示例中,配置文件可以从创建屏幕导入或手动完成,但用户应该获得所有错误消息,而不是按它们出现的顺序获取每个错误消息。在我正在处理的应用程序中,应用的规则有点复杂,但思路是一样的。我知道我不应该让 UI 问题影响域,但我不想再重复两次所有验证,以便我可以确保命令不会失败,因为这会导致可维护性问题更进一步(我发现自己处于并试图解决的情况!)

Eug*_*ène 4

这个问题可能有点宽泛,围绕着建筑设计,这是你应该决定的事情,但无论如何我都会尽力提供帮助 - 我只是无法控制自己。

首先:这是一篇很棒的文章,可能已经暗示您对自己的设计过于挑剔:http://jeffreypalermo.com/blog/the-fallacy-of-the-always-valid-entity/

您需要决定系统处理验证的方式。

也就是说,您是否想要一个域绝对不会出现一致性失败的系统?然后,您可能需要额外的类来清理您拥有的任何命令,并在接受或拒绝对域(清理层)的更改之前验证它们。或者,如该文章所述,它可能表明处理特定情况需要完全不同类型的对象。(比如不符合当前规则的遗留数据)

当出现严重错误时,域抛出异常是否可以接受?然后丢弃当前聚合(甚至当前上下文)中的所有更改并通知用户。

如果您正在寻找和平的中间解决方案,也许可以考虑这样的事情:

public OperationResult UpdateAccount(IBankAccountValidator validator, IAccountUpdateCommand newAccountDetails)
    {
        var result = validator.Validate(newAccountDetails);
        if(result.HasErrors)
        {
            result.AddMessage("Could not update bank account", Severity.Error);
            return result;
        }

        //apply further logic here

        //return success
    }
Run Code Online (Sandbox Code Playgroud)

现在,您可以将所有验证逻辑放在一个单独的类中,但您必须传递该逻辑并通过双重调度进行调用,并且您将在每次调用中添加如上所示的结果处理。您确实必须决定哪种风格对您/团队来说是可以接受的,以及从长远来看什么是可维护的。

  • 同一网站上的 @StevenBrookes 查看这篇旧文章,了解有关处理错误的更多想法 http://enterprisecraftsmanship.com/2015/03/20/function-c-handling-failures-input-errors。看看面向铁路的编程的思想。 (2认同)