DDD不变量业务规则和验证

Jef*_*f M 18 domain-driven-design dry single-responsibility-principle invariants

我正在寻找有关在何处添加域实体验证规则以及实施最佳实践的建议.我做了搜索,没找到我要找的东西,或者我错过了.

我想知道建议的方法是验证属性不是null,在某个范围或长度等等...我已经看到了几种使用IsValid()和其他关于在构造函数中执行的讨论的方法,所以实体从不处于无效状态,或者使用预处理和后处理,而其他实体使用FluentValidation api,不变量如何影响DRY和SRP.

当使用App Service,Bounded Context,Domain Service,Aggregate Root,Entity layering时,有人能给我一个很好的例子来说明在哪里进行这些检查.这是怎么回事,最好的方法是什么?

谢谢.

Dav*_*New 32

在对域实体进行建模时,最好考虑现实世界的含义.假设您正在与Employee实体打交道.

员工需要一个名字

我们知道,在现实世界中,员工必须始终拥有一个名称.员工不可能没有名字.换句话说,如果没有指定其名称,就无法"构建"员工.所以,使用参数化构造函数!我们也知道员工姓名不能改变 - 因此我们通过创建私人安装员来防止这种情况发生.使用.NET类型系统来验证您的员工是一种非常强大的验证形式.

public string Name { get; private set; }

public Employee(string name)
{
    Name = name;
}
Run Code Online (Sandbox Code Playgroud)

有效名称有一些规则

现在它开始变得有趣了.名称有一定的规则.让我们采用简单化的路由,并假设有效名称是非空或空的名称.在上面的代码示例中,未验证以下业务规则.此时,我们仍然可以创建无效的员工!让我们通过修改我们的二传手来防止这种情况发生:

public string Name
{
    get
    {
        return name;
    }
    private set
    {
        if (String.IsNullOrWhiteSpace(value))
        {
            throw new ArgumentOutOfRangeException("value", "Employee name cannot be an empty value");
        }

        name = value;
    }
}
Run Code Online (Sandbox Code Playgroud)

我个人更喜欢在私有setter中使用这个逻辑而不是在构造函数中.二传手并非完全不可见.实体本身仍然可以改变它,我们需要确保有效性.另外,总是抛出异常!

暴露某种形式的IsValid()方法怎么样?

拿上面的Employee实体.方法在哪里以及如何IsValid()工作?

您是否允许创建无效的Employee,然后期望开发人员通过支票检查其有效性IsValid()?这是一个弱设计 - 在你知道之前,无名的员工将在你的系统周围巡航造成破坏.

但也许您想公开名称验证逻辑?

我们不想捕获控制流的异常.例外情况是灾难性的系统故障.我们也不想在代码库中复制这些验证规则.所以,也许暴露这种验证逻辑并不是一个坏主意(但仍然不是最好的!).

你可以做的是提供一个静态IsValidName(string)方法:

public static bool IsValidName(string name)
{
    return (String.IsNullOrWhiteSpace(value))
}
Run Code Online (Sandbox Code Playgroud)

我们的财产现在会有所改变:

public string Name
{
    get
    {
        return name;
    }
    private set
    {
        if (!Employee.IsValidName(value))
        {
            throw new ArgumentOutOfRangeException("value", "Employee name cannot be an empty value");
        }

        name = value;
    }
}
Run Code Online (Sandbox Code Playgroud)

但是这个设计有点可疑......

我们现在开始为实体的各个属性生成验证方法.如果一个属性附加了各种规则和行为,也许这表明我们可以为它创建一个值对象!

public PersonName : IEquatable<PersonName>
{
    public string Name
    {
        get
        {
            return name;
        }
        private set
        {
            if (!PersonName.IsValid(value))
            {
                throw new ArgumentOutOfRangeException("value", "Person name cannot be an empty value");
            }

            name = value;
        }
    }

    private PersonName(string name)
    {
        Name = name;
    }

    public static PersonName From(string name)
    {
        return new PersonName(name);
    }

    public static bool IsValid(string name)
    {
        return !String.IsNullOrWhiteSpace(value);
    }

    // Don't forget to override .Equals
}
Run Code Online (Sandbox Code Playgroud)

现在我们的Employee实体可以简化了(我已经排除了空引用检查):

public Employee
{
    public PersonName Name { get; private set; }

    public Employee(PersonName name)
    {
        Name = name;
    }
}
Run Code Online (Sandbox Code Playgroud)

我们的客户端代码现在看起来像这样:

if(PersonName.IsValid(name))
{
    employee = new Employee(PersonName.From(name));
}
else
{
    // Send a validation message to the user or something
}
Run Code Online (Sandbox Code Playgroud)

那我们在这做了什么?

我们确保我们的域模型始终保持一致.非常重要.无法创建无效的实体.此外,我们使用价值对象来提供进一步的"丰富性". PersonName已经为客户端代码提供了更多控制和更多功能,并且还简化了Employee.

  • +1这可能是如何封装我曾经读过的大部分验证需求的最好例子. (3认同)
  • +1很高兴看到真正了解DDD的人的答案,这在StackOverflow上是罕见的! (3认同)
  • 这是一个非常简单(简单化?!)的例子。需要检查数据库的验证规则变得更加困难 - 例如。唯一的名称或电子邮件。那么内部验证或静态方法的帮助就没有这里那么大,或者它们变得臃肿,因此需要分离关注点(例如单独的验证类)。当员工具有基于他来自哪里(国家)或他/她所在的系统(例如系统 A 或来自远程系统 B)的姓名验证规则(例如,是否需要中间名)时,我们不要忘记上下文相关的验证)。 (2认同)