Cas*_*ins 7 .net validation domain-driven-design
我正在编写一个基于MVVM的VB.NET 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
Run Code Online (Sandbox Code Playgroud)
然后我跑过Data Annotations,看起来很漂亮.我注意到大多数使用数据注释的人允许域实体暂时变为无效,然后在稍后通过调用Validate.ValidateObject来验证实体.此时,实体无效,原始状态已丢失,除非您有其他机制将其回滚.
两个问题:
1)您是否允许域名实体暂时无效?
2)根据您对#1的回答,您使用哪些技术来验证实体?
你的直觉是对的.在DDD中,永远不允许对象从域透视图进入无效的状态.即使是暂时的 对象应该保护它们的内部不变量,这是非常基本的OOP.否则它不会是一个对象,而只是一个愚蠢的数据容器.很多时候,人们会对UI框架感到困惑,或者他们过度概括了"验证"一词.
例如,系统中的每个产品都应具有SKU.或者每个客户都应该有社会安全号码.执行这些规则是产品和客户实体的直接责任.好旧的ArgumentNullException会让客户端代码意识到它打破了一些不变量.执行此规则不是UI或某些抽象"验证者"的责任.如果您允许在您的实体之外强制执行此规则,您将:
最终会导致无效状态,这可能导致崩溃或需要您编写一些补偿代码以避免此崩溃
更重要的是,您只需阅读产品代码就无法推断产品
此外,业务规则通常比这更复杂,因此在不破坏其封装的情况下在实体之外强制执行它们会更加困难.通过使用DDD 值对象可以轻松实施另一组规则.在上面的示例中,您将创建类"SKU"和"SocialSecurityNumber".这些类将是不可变的,并将强制执行所有格式规则.他们也可以使用静态方法:
SocialSecurityNumber.TryParse(String untrustedString, out SocialSecurityNumber)
Run Code Online (Sandbox Code Playgroud)
要么
SocialSecurityNumber.IsStringValid(String untrustedString)
Run Code Online (Sandbox Code Playgroud)
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;
}
Run Code Online (Sandbox Code Playgroud)
关于此代码最重要的事情是,即使在更改实体之前也会检查某些验证规则。此示例显示了结果集的用法,因为我经常需要提供有关验证的信息,即使它成功了(换句话说,我的验证失败了,并且必须向用户显示有关它的信息;但是域实体仍然有效。
和是非常简单的基础设施类,允许通过流畅的界面轻松添加新的验证器OperationResultSet。ValidatorOf验证器被实现为实现IValidator接口的类,这允许实现相当复杂的验证规则,并且也更容易单独测试它们。
我的观点是,应该在对域实体进行更改之前执行验证 - 通过正确的约定和一些基础设施,它甚至可以简化代码结构。
编辑说明:由于对此答案有一些批评的声音,我决定将示例代码更改为抛出异常而不是返回结果的代码。尽管我仍然相信这是适合我的场景的方法,但我同意,如果没有指定完整的上下文,这可能会产生误导 - 例外确实应该是第一个选项,并且应该存在其他因素来选择替代方案。
| 归档时间: |
|
| 查看次数: |
1546 次 |
| 最近记录: |