如何对待汇总之间引用的验证?


11

我在汇总之间的引用方面有些挣扎。让我们假设该聚合Car引用了该聚合Driver。此参考将通过具有进行建模Car.driverId

现在我的问题是,我应该走多远的时间才能验证在中创建Car集合CarFactory。我应该相信传递的对象DriverId现有对象, Driver还是应该检查该不变量?

对于检查,我看到两种可能性:

  • 我可以更改汽车制造厂的签名以接受完整的驾驶员实体。然后,工厂将仅从该实体中选择ID,并以此为单位构建汽车。这里不变量被隐式检查。
  • 我可以DriverRepositoryCarFactory和显式调用中引用driverRepository.exists(driverId)

但是现在我想知道是否有太多的不变性检查?我可以想象那些聚合可能生活在单独的有界上下文中,现在我将依赖于DriverBC的DriverRepository或Driver实体污染汽车BC。

而且,如果我与领域专家交谈,他们将永远不会质疑此类引用的有效性。我感觉到我的领域模型受到无关的关注。但是话又说回来,在某个时候,用户输入应该得到验证。

Answers:


6

我可以更改汽车制造厂的签名以接受完整的驾驶员实体。然后,工厂将仅从该实体中选择ID,并以此为单位构建汽车。这里不变量被隐式检查。

这种方法很吸引人,因为您可以免费获得支票,并且与普遍使用的语言保持一致。A Car不是由a驱动driverId,而是由a 驱动Driver

沃恩·弗农(Vaughn Vernon)实际上在其Identity&Access示例的有界上下文中使用了这种方法,在该上下文中,他将一个User聚合传递到一个Group聚合,但Group仅适用于值类型GroupMember。如您所见,这还使他能够检查用户的启用情况(我们很清楚该检查可能已过时)。

    public void addUser(User aUser) {
        //original code omitted
        this.assertArgumentTrue(aUser.isEnabled(), "User is not enabled.");

        if (this.groupMembers().add(aUser.toGroupMember()) && !this.isInternalGroup()) {
            //original code omitted
        }
    }

然而,通过传递Driver情况下,你还开自己给的意外修改DriverCar。传递值引用使从程序员的角度更容易推理出变化,但与此同时,DDD与泛在语言有关,因此值得冒险。

如果您实际上可以拿出好名字来应用接口隔离原则(ISP),那么您可能依赖于没有行为方法的接口。您也许还可以提出一个值对象概念,该概念表示不可变的驱动程序引用,并且只能从现有驱动程序(例如DriverDescriptor driver = driver.descriptor())实例化。

我可以想象那些聚合可能生活在单独的有界上下文中,现在我将依赖于DriverBC的DriverRepository或Driver实体污染汽车BC。

不,您实际上不会。总会有一个反腐败层,以确保一个上下文的概念不会渗入另一个上下文。它实际上是容易得多,如果你有一个专门的汽车司机协会BC,因为你可以模拟现有的概念,如CarDriver专门用于这方面。

因此,您可能DriverLookupService在BC中有一个负责管理汽车驾驶员关联的定义。该服务可以调用由驱动程序管理上下文公开的Web服务,该服务返回Driver在该上下文中很可能是值对象的实例。

请注意,Web服务不一定是BC之间最好的整合方法。您还可以依赖消息传递,例如,UserCreated来自驱动程序管理上下文的消息将在远程上下文中使用,该消息会将驱动程序的表示形式存储在其自己的数据库中。DriverLookupService然后,他们可以使用该数据库,并且驱动程序的数据将与其他消息(例如DriverLicenceRevoked)保持最新。

我无法真正告诉您哪种方法更适合您的领域,但是希望这将为您提供足够的见识以做出决定。


3

您提出问题的方式(并提出了两个替代方案),似乎唯一需要关注的是在创建汽车时driverId仍然有效。

但是,您还必须担心,与driverId相关联的驱动程序在删除汽车或指定其他驱动程序之前不会被删除(并且可能还没有将该驱动程序分配给另一辆汽车(如果域将驱动程序限制为仅与一辆汽车相关联))。

我建议您分配而不是进行验证(这将包括在场验证)。然后,您将禁止删除仍在分配的删除,从而防止在构造过程中陈旧数据的争用情况以及其他长期问题。(请注意,分配既验证又标记分配,并且自动进行操作。)

顺便说一句,我同意@PriceJones的观点,即汽车和驾驶员之间的关联可能是与汽车或驾驶员无关的责任。这种关联只会随着时间的推移而变得越来越复杂,因为这听起来像是一个日程安排问题(驾驶员,汽车,时隙/窗户,替代品等)。注册以及当前注册。因此,它可能完全值得拥有自己的BC。

您可以在要分配的聚合实体的BC内,或在一个单独的BC(例如负责在汽车与驾驶员之间建立关联的BC)内提供一种分配方案(例如布尔值或引用计数)。如果使用前者,则可以允许发布给汽车或驾驶员BC的(有效)删除操作;如果您选择后者,则需要防止从BC的汽车和驾驶员那里删除,而是通过CAR和驾驶员协会调度程序将它们发送出去。

您还可以按以下方式在BC之间分配一些分配职责。汽车和驾驶员BC各自提供一个“分配”方案,该方案用该BC验证并设置分配的布尔值;当设置了它们的分配布尔值时,BC会阻止删除相应的实体。(并且已设置系统,因此汽车和驾驶员BC仅允许从汽车/驾驶员关联调度BC进行分配和释放。)

然后,汽车和驾驶员调度BC会在当前和将来的某些时间段/期限内维护与汽车相关的驾驶员日历,并仅在最后一次使用调度的汽车或驾驶员时才通知其他BC解除分配。


作为更根本的解决方案,您可以将BC的汽车和驾驶员视为仅追加历史记录的工厂,而所有权归于car / driver协会调度程序。BC车可能会生成一辆新车,其中包含该车的所有详细信息以及其VIN。汽车的所有权由汽车/驾驶员协会调度程序处理。即使汽车/驾驶员协会被删除,汽车本身也被销毁,汽车的记录依定义仍然存在于汽车BC中,我们可以使用汽车BC查找历史数据。而汽车/驾驶员协会/所有权(过去,现在和将来可能的预定)则由另一个BC处理。


2

假设聚合的Car引用了聚合的Driver。该引用将通过具有Car.driverId进行建模。

是的,这是将一个聚集体耦合到另一个聚集体的正确方法。

如果我要与领域专家交谈,他们将永远不会质疑此类引用的有效性

问您的域专家不是一个正确的问题。尝试“如果不存在驱动程序,对企业造成的成本是多少?”

我可能不会使用DriverRepository来检查driverId。相反,我将使用域服务来做到这一点。我认为这样做可以更好地表达意图-在幕后,域名服务仍在检查记录系统。

所以像

class DriverService {
    private final DriverRepository driverRepository;

    boolean doesDriverExist(DriverId driverId) {
        return driverRepository.exists(driverId);
    }
}

您实际上是在许多不同的地方查询有关driverId的域

  • 从客户端发送命令之前
  • 在应用程序中,在将命令传递给模型之前
  • 在域模型内,在命令处理期间

这些检查中的任何一项或全部都可以减少用户输入中的错误。但是它们都在过时的数据中工作。我们提出问题后,其他总量可能立即发生变化。因此,总会有假阴性/阳性的危险。

  • 在异常报告中,在命令完成后运行

在这里,您仍在使用过时的数据(运行报表时聚合可能正在运行命令,您可能看不到对所有聚合的最新写入)。但是聚合之间的检查永远不会是完美的(Car.create(driver:7)与Driver.delete(driver:7)同时运行)因此,这为您提供了额外的防御风险层。


1
Driver.delete不应该存在。我从未真正看到过聚集体被破坏的领域。通过保持AR周围,您永远不会成为孤儿。
plalx '16

1

问一下可能会有所帮助:您确定汽车是由驾驶员制造的吗?我从未听说过现实世界中由驾驶员组成的汽车。这个问题之所以重要,是因为它可能会指导您独立创建汽车和驾驶员,然后创建一些将驾驶员分配给汽车的外部机制。汽车可以在没有驾驶员参考的情况下存在,并且仍然是有效的汽车。

如果汽车在您的上下文中必须绝对有驾驶员,那么您可能要考虑制造商模式。这种模式将负责确保使用现有驾驶员来构造汽车。工厂将为经过独立验证的汽车和驾驶员提供服务,但制造商将确保汽车在为汽车提供服务之前具有所需的参考。


我也考虑过汽车/驾驶员的关系-但引入DriverAssignment聚合只是需要验证参考的动作。
VoiceOfUnreason

1

但是现在我想知道是否有太多的不变性检查?

我认同。如果不存在,则从数据库中获取给定的DriverId将返回一个空集。因此,检查返回结果使询问它是否存在(然后提取)变得不必要。

然后课堂设计也使它不必要

  • 如果有要求“停放的汽车可能有也可能没有驾驶员”
  • 如果Driver对象需要一个DriverId并且在构造函数中设置。
  • 如果Car仅需要DriverId,请使用Driver.Id吸气剂。没有二传手。

存储库不是业务规则的地方

  • A会Car关心它是否具有一个Driver(或至少具有他的ID)。一个Driver关心是否有一个DriverId。的Repository有关数据完整性关心和可能不关心司机,私家车的减少。
  • 数据库将具有数据完整性规则。非空键,非空约束等。但是数据完整性是关于数据/表模式,而不是业务规则。在这种情况下,我们具有密切相关的共生关系,但不要将两者混为一谈。
  • a DriverId是业务领域事物的事实在适当的类中处理。

分离关注点违规

... Repository.DriverIdExists()问问题时发生。

构建一个域对象。如果不是,Driver那么可能是一个对象DriverInfo(假设是DriverIdand Name)。该DriverId构造时被验证。它必须存在,并且是正确的类型,以及其他任何类型。然后是一个客户端类设计问题,如何处理不存在的driver / driverId。

也许Car没有司机就好,直到您致电Car.Drive()。在这种情况下,Car对象当然会确保其自身的状态。没有Driver-就不能开车。

将属性与其类分开是不好的

当然可以,Car.DriverId如果您愿意的话。但它看起来应该像这样:

public class Car {
    // Non-null driver has a driverId by definition/contract.
    protected DriverInfo myDriver;
    public DriverId {get { return myDriver.Id; }}

    public void Drive() {
       if (myDriver == null ) return errorMessage; // or something
       // ... continue driving
    }
}

不是这个:

public class Car {
    public int DriverId {get; protected set;}
}

现在Car必须处理所有DriverId有效性问题-违反单一责任原则;和冗余代码。

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.