是否允许域对象暂时无效,以及该决定如何影响验证技术



我正在写一个VB。. NET基于MVVM的Winforms项目(使用Winforms绑定)。我的直觉是永远不允许域实体处于无效状态。这要求我在新实体的构造函数和现有实体的每个setter中进行验证检查:

Public Class Product

    Public Sub New(ProductID as Integer, Name as String)
        If ProductID > 0 AndAlso Name.Length > 5 Then
            _ProductID = ProductID
            _Name = Name
        Else
            Throw New InvalidProductException
        End If
    End Sub
    Private _ProductID as Integer
    Public Property ProductID as Integer
        Set(value as String)
            If value > 0 then
                _ProductID = value
            Else
                Throw New InvalidProductException
            End If
        End Set
    End Property
    'Same principle as above for Name setter.

End Class

然后我遇到了Data Annotations,它看起来很漂亮。我注意到,大多数使用数据注释的人允许域实体暂时无效,然后在稍后的某个时刻通过调用validate . validateobject来验证该实体。此时,实体无效,原始状态已经丢失,除非您有其他机制将其回滚。

两个问题:

1)你允许域名实体暂时失效吗?

2)根据你对#1的回答,你使用什么技术来验证实体?

你的直觉是对的。在DDD中,不应该允许对象进入从域的角度看无效的状态。即使是暂时的。对象应该保护它们的内部不变量,这是非常基本的OOP。否则它就不是一个对象,而只是一个愚蠢的数据容器。很多时候,人们会对UI框架感到困惑,或者他们把"验证"这个术语泛化了。

例如,系统中的每个产品都应该有SKU。或者每个客户都应该有社会安全号码。执行这些规则是Product和Customer实体的直接责任。老的ArgumentNullException会让客户端代码意识到它破坏了一些不变量。强制执行这个规则不是UI或某个抽象的"验证器"的责任。如果您让此规则在您的实体之外强制执行,您将:

  • 最终以无效状态结束,这可能导致崩溃或需要您编写一些补偿代码来避免崩溃

  • 更重要的是,您将无法通过阅读产品代码来推断产品

此外,业务规则通常比这更复杂,因此在不破坏实体封装的情况下在实体外部执行它们将更加困难。还有另一组规则可以通过使用DDD值对象轻松执行。在上面的示例中,您将创建类"SKU"one_answers"SocialSecurityNumber"。这些类将是不可变的,并将强制执行所有格式化规则。它们也可以有静态方法,如

SocialSecurityNumber.TryParse(String untrustedString, out SocialSecurityNumber)

SocialSecurityNumber.IsStringValid(String untrustedString)

UI可以使用这些方法来验证用户输入。UI没有理由"破坏"对象,即使是暂时的。如果你让这种情况发生,你最终会得到一个贫血的领域模型。不幸的是,互联网上的许多示例代码都在推广这种方法。底线是,您的验证规则来自您的域,它们应该由域对象强制执行。

首先,不,域对象不应该以无效状态存在。

我认为你的困惑源于你试图从域对象(你肯定不是唯一一个)中为你的UI屏幕提供动力的事实,即绑定到你的域对象属性。你不应该。你的UI应该从视图模型的属性中获取数据;一个专门构建的UI对象,允许处于无效状态,并且可以利用任何很酷的UI验证框架。

域对象应该只在命令/事务的上下文中发挥作用。例如,用户从屏幕上的列表中选择了一个视图模型对象,并希望对其执行操作。通常情况下,UI将调用应用程序/命令服务方法来执行他们希望执行的操作,并从用户选择的视图模型中传递ID。然后,这将从它的存储库中检索域对象并调用适当的方法。

关于视图模型对象的来源:我有一个单独的查询服务,由UI调用。这提供了"扁平"(非规范化聚合数据)DTO,可用于填充视图模型对象或充当视图模型本身。此服务不知道任何域模型,只是从域对象执行事务的数据中返回投影。

我不能推荐足够的阅读CQRS,即使你只采取它的某些方面。它不仅有助于使具有实用的 DDD意义,而且如果实现得好,它还可以真正帮助将数据获取到UI屏幕的性能方面。

不,在我看来,域实体永远不应该是无效的,即使是暂时的。问题是,如果您允许域无效,就像您在问题中描述的那样,随着复杂性的增长,引入新规则变得困难。例如,您允许实体由于某些属性而无效,假设稍后将对其进行验证。但在此之前,有人添加了另一个规则,根据相同的属性改变其结果-您如何知道规则是否正确运行?你不。相信我,这在重要的领域经常发生。

不允许状态无效的另一个原因是,在某些情况下,它可能会引入orm问题——我个人看到过一个涉及NHibernate缓存和子实体无效的问题,但不知怎么的仍然留在缓存中,我不记得任何具体的细节了。

我倾向于使用基于验证规则和验证结果的技术。简而言之,实体上的大多数方法都是通过以下方式实现的(c#,如果你不介意的话):

        public virtual void ChangeClaimEventDate(DateTimeOffset newDate)
        {
            var operationResult = ValidatorOf<Claim>
                .Validate()
                .WithCriticalRuleOf<EventDateFallsIntoPolicyCoverage>().WithParam(newDate)
                .WithCriticalRuleOf<EventDateFallsIntoInsuredCoverage>().WithParam(newDate)
                .WithCriticalRuleOf<PerformedServicesAreAvailableOnEventDate>().WithParam(newDate)
                .WithCriticalRuleOf<EventDateCannotBeChangedForBilledClaim>().WithParam(newDate)
                .ForOperation(this);
            if (operationResult.OperationFailed)
            {
                throw new InvalidBusinessOperation(operationResult);
            }
            SomeDate = newDate;
        }

这段代码最重要的一点是,某些验证规则甚至在实体被更改之前就被检查了。这个例子展示了结果集的用法,因为我经常需要提供关于验证的信息,即使它成功了(换句话说,我有失败的验证,并且必须向用户显示有关它的信息;但是,域实体仍然有效。

OperationResultSetValidatorOf是非常简单的基础结构类,允许通过流畅的接口轻松添加新的验证器。验证器被实现为实现IValidator接口的类,它允许实现相当复杂的验证规则,并且更容易单独测试它们。

我的观点是,验证应该在对域实体进行更改之前执行——通过正确的约定和一些基础设施,它甚至可以简化代码结构。

编辑说明:由于这个答案的一些批评声音,我决定将示例代码更改为抛出异常而不是返回结果的代码。尽管我仍然相信这是适合我的场景的方式,但我同意,如果没有指定完整的上下文,这可能会产生误导——例外确实应该是第一种选择,并且应该存在其他因素来选择替代方案。

最新更新