在执行领域驱动设计时如何正确利用数值对象验证?

6

我有一段伪代码,描述了一个简单的实体 Order:

class Order{    
    private int quantity;
    private Date orderDate;
    private Date shippingDate;

    public Order(int quantity, Date orderDate, Date shippingDate){
        if(quantity <= 0){ throw new Exception("Invalid quantity")}
        if(shippingDate < orderDate){ throw new Exception("Invalid shippingDate")}
        if(...more validation...){....throw Exceptions...}

       //assign values if everything is OK
    }
}

描述、数量、订单日期和发货日期都是从网络表单中读取的,每个表单都是一个文本字段,由多个验证器进行配置:

quantityField= new TextField('txt_quantity');
quantityFiled.addNotNullValidator().addNumaricValidator().addPositiveIntegerValidator()

如您所见,验证逻辑在TextField验证和实体验证之间是重复的。
我尝试通过创建Quantity类、OrderDate类和ShippingDate类来向我的实体引入值对象的概念。因此,我的Order实体变成了这样:

class Order{    
    private Quantity quantity;
    private OrderDate orderDate;
    private ShippingDate shippingDate;

    public Order(Quantity quantity, OrderDate orderDate, ShippingDate shippingDate){
        //assign values without validation I think??!!
    }
}

例如,类“Quantity”将会是:

class Quantity{

private int quantity;
public Quantity(int quantity){
        if(quantity <= 0){ throw new Exception("Invalid quantity")}
        this.quantity=quantity;
}

}

现在来回答这些问题:

  1. 聚合根难道不应该负责验证整个聚合吗?我的 Quantity 类违反了这个原则吗?
  2. 如何重用 Quantity 构造函数中的验证逻辑,以在 web 表单验证中使用?我认为验证代码实际上是重复的,所以如何才能验证一次或至少重用验证逻辑。
  3. 既然所有值对象都将自行验证,那么这是否意味着我不应该在实体中验证任何内容?
  4. 由于 ShippingDate 的验证取决于 OrderDate,那么我应该如何验证发货日期?
  5. DDD 工厂在所有这些中起到了什么作用?

你说你有一个实体Customer,但你展示的是Order的代码。 - Daniel Hilgarth
@DanielHilgarth 对不起,是我的错误。我现已修正。 - Songo
将验证逻辑提取到一个返回布尔值的静态方法中,以便在值对象之外重用验证变得简单。构造函数将调用该方法,并在其返回false时抛出异常。Web层可以使用Quantity.IsValid(value)进行验证。 - MikeSW
正如已经提到的,你的问题不在于有或没有对象值。不必要地添加它们并不能使事情变得更容易。你需要分开两种类型的检查:1)检查是否应用了域级约束;2)检查用户输入、Web级别验证。 - Nazar Merza
3个回答

3
  1. 如果在您的领域中任何数量都不能为负数,无论上下文如何,这是完全有道理的。
  2. 在我看来,您数量构造函数中的验证是为了确保应用程序正确使用类。它会抛出异常,这些异常是针对异常状态而设计的,而不是预期工作流程。因此,它具有与Web表单验证完全不同的目的,后者可以确保用户正确使用您的应用程序。它期望无效输入并处理它。我认为这里没有真正的重复,至少不会违反单一职责原则。
  3. 我不认为是这种情况。您有if(shippingDate < orderDate) - 您计划如何在值对象内进行验证?
  4. 啊,您看到了问题。相同的答案:此验证属于订单实体。此外,您不必为所有内容都使用值对象。如果订单日期或发货日期本身没有任何约束,请继续使用日期
  5. 这似乎是一个单独的问题,我没有看到任何与值对象相关的相关性。

好的答案。对于第二点,我想要补充一下:总的来说,这是一个非常有趣的问题,却没有很好的解决方案。虽然整合验证规则会很不错,但由于语言和框架的限制,这可能很困难。例如,ASP.NET MVC 中的验证会将验证属性转换为客户端 JavaScript。这种方法以及相关方法的问题在于,必须使用只有在被调用时才能保护实体的属性。此外,验证上下文可能会影响规则本身,这是分割责任的另一个原因。 - eulerfx

2
  1. 聚合根应该在其聚合中强制执行不变量,但它们并不执行所有验证。特别是在构造函数或工厂中处理的构造时验证通常不会被执行。事实上,尽可能多地将(非特定上下文的)不变量移动到构造函数和工厂可能是有益的。我认为拥有始终有效的实体比依靠对聚合根或实体本身重复使用ValidateThis()ValidateThat()方法更好。

  2. 基本上有三种验证:客户端验证、应用程序验证(在控制器或应用程序层服务中)和领域验证(领域层)。客户端验证是必需的,不能被重用。应用程序验证可以依赖于领域验证,在您的示例中意味着只需调用数量构造函数并处理抛出的异常即可。但它也可以具有自己的一组应用程序特定的、非领域规则 - 例如,针对其password_confirm验证password字段。

  3. 始终有效的实体相同的精神,值对象最好是不可变的,这意味着您只需在新建时验证一次即可。但这是固有的验证,您可以在包含实体中进行外围验证(例如,您不能在列表中有超过3个该类型的值对象值对象A始终与值对象B一起使用等)。

  4. 这是情境验证,而不是ShippingDate固有不变量的验证。因此,订单应负责检查ShippingDate >= OrderDate是否独立于每个这些值对象的有效性。

  5. 当一个对象的构造逻辑足够复杂以至于它本身就是一个职责时,并且由于SRP的原因不适合于对象的构造函数或消费者时,应使用工厂。工厂确实包含构造时验证逻辑,就像构造函数一样,这使它们成为某种不变量执行者。


0
这是许多问题,你可能想将它们拆分为单个问题。
第2到5个问题非常依赖于各种因素,因此可因人而异。
但这是我对问题1的答案(并且在某种程度上也回答了问题3和4): 聚合负责其完整性。不是聚合根。只要整个聚合保持有效,聚合中的每个项都可以执行自己的验证。
状态验证(如正确数量或数量不为负)可以在各自的类中进行。像ShippingDate > = OrderDate这样的相互依赖的状态验证可以在更高级别上完成,例如在聚合根中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接