【问题标题】:Does entity really can belong to only one aggregate?实体真的只能属于一个聚合吗?
【发布时间】:2019-08-28 13:28:50
【问题描述】:

我正在学习 DDD,但遇到了一个我无法解决的问题。 假设我们有以下域:

public class Hotel : AggregateRoot {
    public List<Room> Rooms { get; private set; }
}

public class Room : Entity {
    public string Name { get; set; }
    public int Number { get; set; }
}

现在我们要为 RoomReservations 建模。

public class RoomReservationRecord : Aggregate {
    public string CustomerName { get; set; }
    public Room Room { get; set; } // <- this is problem
    public DateTime DateFrom { get; set; }
    public DateTime DateTo { get; set; }
}

清晰可见,2 个聚合包含(共享)单个实体。从业务角度来看是有道理的,但从 DDD 角度来看,看起来 2 个聚合共享同一个实体。

  1. 这种方法是否正确,还是违反了“实体可以成为单个聚合的一部分”规则?
  2. 或者有更好(明显的方式)来模拟这种需求?

【问题讨论】:

  • 此类问题强烈表明您没有正确识别聚合根。这里的问题是:Hotel 真的是聚合根吗?或者 RoomReservationRecord 不应该是一个聚合?或者Room 根本就不是一个实体,只是一个值类型。聚合也可以引用其他聚合,但只能通过其标识符(即 RoomId 而不是引用聚合上的 Room
  • 我也有同样的想法,但后来我意识到没有酒店就没有房间。因此,恕我直言,腾出空间聚合本身感觉不对。作为 VO 的房间也感觉不对,因为在现实生活中房间是独立的实体。它们可以通过唯一的编号来识别,另一个相同的房间不是同一个房间。我也可以将房间设为一个集合,但除非我们引入 Parent 参考,否则房间将有没有酒店的风险......这是一个非常酷的想法 tbh
  • 对,但是对于房间预订来说,重要的是房间号和酒店(如果有多个酒店),可以很好地表示为值类型。值类型由其值标识,因此酒店+房间号几乎可以唯一识别它。当您只需要与房间的关联时,您为什么认为您需要一个完整的房间实体来为您的 RoomReservationRecord 聚合?
  • 因为 Room 是一个实体。在获取 RoomReservation 记录时,DDD 的重点是在其中包含一个 Room 对象,而不是它的 ID。它仍然可以使用 ID,但恕我直言,OOP 和 DDD 即将在内存中生成对象图。
  • 在获取 RoomReservation 记录时,DDD 的重点是在其中有一个 Room 对象,这并不真正正确。聚合根的要点是充当事务边界,在聚合内是在单个事务中完成的。房间作为 RoomReservation 的实体是没有意义的,因为 Room 可以在没有 RoomReservation 的情况下存在,所以让它成为 RoomReservation 的实体是完全错误的。如果没有HotelRoom 怎么会存在,所以这听起来更像是正确的聚合,以及为什么保留的 ID 更有意义。

标签: domain-driven-design


【解决方案1】:

我会尝试指出一些事情,但归根结底,这是一个建模练习,通常需要做出妥协。

不变量 - 在对象图中对对象进行分组的一个重要部分(可能是主要部分)是确保执行某些规则。因此,如果任何房间都不能被双重预订是一个业务规则,那么Hotel 可能是一个聚合根,其中房间和预订是其上的实体。

类似这样的东西......但这有一些缺点......

EG。

class Hotel
{
    //members (eg. Rooms and Reservations) ...
    public Hotel(string name, ICollection<Room> rooms)
    {
        //...
    }

    bool TryMakeReservation(Reservation reservation)
    { 
        // if booked already return false
        return true;
    }
}

class Room
{
    //members ...
    public Room(int number, bool isBooked)
    {
        //..
    }
}

class Reservation  {
    public string CustomerName { get; private set; }
    public int RoomNumber { get; private set; }
    public DateTime DateFrom { get; private set; }
    public DateTime DateTo { get; private set; }

    Reservation(Parameters)
    {
        // ...
    }
}

注意:要使用此模式,您需要在每次预订时锁定酒店!

这对于繁忙的酒店来说可能是不可接受的。有一些方法可以解决这个问题,例如保留 5 分钟,然后自动释放,除非确认预订。或者存储一个事件列表,如果同一房间在同一时期存在 2 个预订而没有结帐事件,则启动一个流程以通知某人处理重复预订。

上下文 - 房间列表和实际预订可能位于不同的域中。想想从多个来源(如 AirBnb、Booking.com、酒店网站和/或在柜台亲自或通过电话)进行预订的频率。将预订和可用房间的持久性放在同一个域中可能没有意义。清洁时间表怎么样。房间需要在可用之前进行清洁,但这真的是在 Booking 上下文中处理的吗?

性能 - 如前所述,有时我们想要的模型是不可能的,因为从数据存储中获取大量数据的物理特性。如果模型对性能的影响太大,用户、产品负责人等往往不会关心模型的干净程度。

Repositories - 从上述观点开始,由于聚合应该是一致的类型(即其中的数据永远不应该处于不正确的状态),那么当您获取聚合时,它应该是持续的。如果Hotel 有一个存储库但包含一个房间,而Room 是一个聚合根并且有自己的存储库,并且存储库正在调用存储库,我会说你在扩展复杂性方面失败了。 DDD 的重点是提供一组模式和实践来帮助您处理复杂性。如果通过应用 DDD 原则,您将复杂性增加了一个步骤,而随着新功能的添加,未来更小的复杂性步骤永远不会利用这一步骤,那么 DDD 可能不是项目中那个时候使用的正确工具。

与有关 Ids 的链接文章交谈。只是不使用原始类型可以减轻这里的很多问题。专注于寻找Value Objects 可以提供很多清晰度,并真正有助于表达您的领域。即使您不使用 DDD,这也是一种有价值的做法,这就是为什么 I wrote a series on them without ever mentioning Value Objects... I think

我希望这会有所帮助。在我看来,DDD 甚至比 FP 还要多,其中包含许多非常有价值的想法,可以帮助创建可根据需求扩展的可维护代码。更好的是,它专注于代码之外的软元素,例如协作,并共享一种带来更多价值的语言,但它们只是需要应用的指导方针,因为您想要获得一定的收益(并且愿意支付任何产生的成本) .它们不是适用的规则,而且很少只是一条错误和正确的道路。

【讨论】:

  • 所以在我的例子中 RoomReservation 是一个不同的有界上下文?如果“是”,这意味着这些实体(Hotel and Room vs RoomReservation)不应该相互了解吗?进一步建模持久性(关系数据库),RoomReservations 表不应该有 FK 到 Rooms 表?
  • 这完全取决于问题。我的观点更多的是某些词,例如Room,可能会出现在多种上下文中。有些人会从另一个上下文中获取,有些人可能有一个包含的想法(不太可能)。这些关系取决于需要强制执行的规则,而不是您认为实体应该具有什么关系或什么应该是聚合根。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-14
  • 1970-01-01
相关资源
最近更新 更多