我们应该在哪里验证领域模型


38

我仍在寻找域模型验证的最佳实践。将验证放入域模型的构造函数中好吗?我的域模型验证示例如下:

public class Order
 {
    private readonly List<OrderLine> _lineItems;

    public virtual Customer Customer { get; private set; }
    public virtual DateTime OrderDate { get; private set; }
    public virtual decimal OrderTotal { get; private set; }

    public Order (Customer customer)
    {
        if (customer == null)
            throw new  ArgumentException("Customer name must be defined");

        Customer = customer;
        OrderDate = DateTime.Now;
        _lineItems = new List<LineItem>();
    }

    public void AddOderLine //....
    public IEnumerable<OrderLine> AddOderLine { get {return _lineItems;} }
}


public class OrderLine
{
    public virtual Order Order { get; set; }
    public virtual Product Product { get; set; }
    public virtual int Quantity { get; set; }
    public virtual decimal UnitPrice { get; set; }

    public OrderLine(Order order, int quantity, Product product)
    {
        if (order == null)
            throw new  ArgumentException("Order name must be defined");
        if (quantity <= 0)
            throw new  ArgumentException("Quantity must be greater than zero");
        if (product == null)
            throw new  ArgumentException("Product name must be defined");

        Order = order;
        Quantity = quantity;
        Product = product;
    }
}

感谢您的所有建议。

Answers:


47

马丁·福勒(Martin Fowler)关于这一主题的一篇有趣的文章强调了大多数人(包括我)往往忽略的一个方面:

但是,我认为不断使人们绊倒的一件事是,当他们认为对象有效性是以isValid方法所暗示的上下文无关方式进行时。

我认为将验证视为绑定到上下文的东西(通常是您要执行的操作)要有用得多。该订单有效吗?该客户有效吗?因此,与其使用isValid这样的方法,不如使用isValidForCheckIn这样的方法。

由此得出,构造函数应该不会做验证,所有的上下文也许除了一些非常基本的完整性检查共享。

再次从文章:

艾伦·库珀(Alan Cooper)在《关于面孔》一书中主张,我们不应让我们对有效状态的想法阻止用户输入(并保存)不完整的信息。几天前,当我阅读吉米·尼尔森(Jimmy Nilsson)正在研究的一本书的草稿时,使我想起了这一点。他提出了一个原则,即即使对象中有错误,也应始终能够保存它。虽然我不认为这应该是绝对的规则,但我确实认为人们倾向于防止储蓄超过应有的水平。考虑验证的上下文可能有助于防止这种情况。


谢天谢地有人这样说。拥有90%数据但不保存任何内容的表单对用户不公平,这些用户经常组成另外10%只是为了不丢失数据,因此所有验证操作都迫使系统失去对其中10%的跟踪组成了。后端可能会发生类似的问题-例如数据导入。我发现通常最好尝试对无效数据进行处理,而不是防止其发生。
psr

2
@psr如果您的数据不持久,您甚至需要后端逻辑吗?如果您的数据对业务模型没有任何意义,则可以将所有操作留在客户端。如果数据没有意义,那么还会浪费资源来回发送消息(客户端-服务器)。因此,我们回到了“绝不允许域对象进入无效状态!”的思想。。
Geo C.

2
我想知道为什么要这么多模棱两可的答案来投票。使用DDD时,有时会有一些规则仅检查某些数据是INT还是在范围内。例如,当您让您的应用用户选择对其产品的一些限制时(某人可以预览我的产品多少次,以及一个月的间隔时间)。在这里,两个约束都应为int,其中之一应在0-31的范围内。似乎是在非DDD环境中适合服务或控制器的数据格式验证。但是在DDD中,我支持将验证保留在域中(占90%)。
Geo C.

2
强制高层对域保持过多了解以使其保持有效状态,这听起来就像是糟糕的不良设计。该域应该是保证其状态有效的域。在高层的肩膀上移动太多会导致您的域贫乏,并且您可能会遇到一些重要的约束,这些约束可能会损害您的业务。我现在意识到,适当的概括是使您的验证尽可能接近您的持久性,或者尽可能接近您的数据操作代码(当对它进行操作以达到最终状态时)。
Geo C.

PS我不将授权(允许做某事),身份验证(消息来自正确的位置或由正确的客户端发送,均由api密钥/令牌/用户名或其他任何方式标识)与格式验证混合使用或业务规则。当我说90%时,我的意思是那些大多数都包含格式验证的业务规则。当然,格式验证可以在高层中进行,但大多数验证将在域中(甚至将在EmailAddress值对象中验证的电子邮件地址格式)。
Geo C.

5

尽管事实上这个问题有些陈旧,但我还是要添加一些有价值的内容:

我想同意@MichaelBorgwardt并通过提出可测试性进行扩展。在“有效地使用旧版代码”中,迈克尔·费瑟斯(Michael Feathers)谈到了测试的障碍,其中的障碍之一是“难以构造”对象。构造一个无效的对象应该是可能的,并且正如Fowler所建议的,上下文相关的有效性检查应该能够识别那些条件。如果您不知道如何在测试工具中构造对象,则将难以测试您的类。

关于有效性,我喜欢考虑控制系统。控制系统通过不断分析输出的状态并在输出偏离设定点时采取纠正措施来工作,这称为闭环控制。闭环控制从本质上预期偏差并采取纠正措施,这就是现实世界的工作原理,这就是为什么所有实际控制系统通常都使用闭环控制器的原因。

我认为使用上下文相关的验证和易于构造的对象将使您的系统更易于使用。


1
很多时候,对象似乎很难构造。例如,在这种情况下,您可以通过创建一个Wrapper类来绕过公共构造函数,该类继承自要测试的类,并允许您以无效状态创建基础对象的实例。这是在类和构造函数上使用正确的访问修饰符的地方,如果使用不当,这确实会对测试产生不利影响。此外,除非适当,否则避免使用“封闭的”类和方法将大大有助于使代码更易于测试。
P. Roe

4

我确定您已经知道...

在面向对象的编程中,类中的构造函数(有时简称为ctor)是在创建对象时调用的一种特殊类型的子例程。它准备使用新对象,通常接受构造函数用来设置首次创建对象时所需的任何成员变量的参数。之所以称为构造器,是因为它构造了类的数据成员的值。

检查作为c'tor参数传入的数据的有效性在构造函数中绝对有效-否则,您可能允许构造无效的对象。

但是(这只是我的意见,目前无法在其上找到任何好的文档)-如果数据验证需要复杂的操作(例如异步操作-如果开发桌面应用程序则可能基于服务器的验证),那么效果会更好放入某种初始化或显式验证函数,并将成员设置为默认值(例如null)。


另外,就像您在代码示例中包括的附带说明一样...

除非您在其中进行进一步的验证(或其他功能),否则AddOrderLine我很可能将公开List<LineItem>为属性,而不是Order充当门面


为什么要暴露容器?什么是上层容器是什么?有一种AddLineItem方法是完全合理的。实际上,对于DDD,这是首选。如果List<LineItem>将其更改为自定义集合对象,则暴露的属性和所有依赖该List<LineItem>属性的内容都可能发生更改,错误和异常。
IAbstract

4

验证应尽快执行。

在任何情况下,无论是域模型还是其他任何编写软件的方式,验证都应达到您要验证的目的以及当前级别的目的。

根据您的问题,我想答案将是分开验证。

  1. 属性验证检查该属性的值是否正确,例如,当范围在1-10之间时。

  2. 对象验证可确保对象上的所有属性相互结合均有效。例如BeginDate在EndDate之前。假设您从数据存储中读取了一个值,并且BeginDate和EndDate都默认初始化为DateTime.Min。设置BeginDate时,没有理由强制执行“必须在EndDate之前”规则,因为这不适用于YET。设置所有属性后,应检查此规则。可以在聚合根级别上调用

  3. 验证也应在聚合(或聚合根)实体上进行。Order对象可能包含有效数据,因此它是OrderLines。但随后的业务规则规定,任何订单都不得超过$ 1,000。在某些允许的情况下,您将如何执行此规则。您不能只添加“不验证金额”属性,因为这会导致滥用(早晚或更晚,甚至您可能只是为了摆脱这种“讨厌的请求”)。

  4. 接下来在表示层进行验证。您是否真的想通过网络发送对象,知道它会失败?或者,您将为用户省去这种burdon,并在他输入无效值后立即通知他。例如,大多数情况下,您的DEV环境将比生产环境慢。您是否要等待30秒,然后再收到“您在又一次测试运行中再次忘记了此字段”的通知,特别是当您的老板喘不过气来解决生产错误时?

  5. 持久性级别的验证应该尽可能接近属性值验证。当使用任何类型的映射器或普通的旧数据读取器时,这将有助于防止读取“空”或“无效值”错误的异常。使用存储过程确实可以解决此问题,但是需要编写相同的评估逻辑AGAIN并再次执行。并且存储过程是数据库管理域,因此不要尝试做HIS工作(或者更糟的是用“他没有得到报酬的挑剔”来打扰他)。

所以用一些著名的词“取决于”来告诉它,但是至少现在您知道为什么要依靠它了。

我希望可以将所有这些都放在一个地方,但是不幸的是,这无法完成。这样做将对包含所有图层的所有验证的“上帝对象”产生依赖性。您不想走那条黑暗的路。

因此,我仅将验证异常抛出属性级别。所有其他级别,我都将ValidationResult与IsValid方法一起使用,以收集所有“破坏的规则”,并在单个AggregateException中将它们传递给用户。

在传播调用堆栈时,然后我再次在AggregateExceptions中收集它们,直到到达表示层。如果WCF作为FaultException,则服务层可以将此异常直接引发给客户端。

这使我可以处理异常,或者将其拆分以在每个输入控件上显示单个错误,或者将其展平并在单个列表中显示。这是你的选择。

这就是为什么我也提到了演示验证,以尽可能地缩短这些验证的原因。

如果您想知道为什么我还要在聚合级别(或您愿意的话,在服务级别)进行验证,那是因为我没有明确的告诉我将来谁将使用我的服务。您将有足够的麻烦查找自己的错误,以防止他人通过输入无效数据而犯错误:。例如,您管理应用程序A,但是应用程序B使用您的服务来提供一些数据。猜猜他们什么时候首先问谁?应用程序B的管理员会愉快地通知用户“我没有错误,我只是输入了数据”。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.