【问题标题】:How does an aggregate root ensure the integrity of the aggregate if it is 'IN' the aggregate?如果聚合根在聚合中,它如何确保聚合的完整性?
【发布时间】:2016-03-18 01:40:48
【问题描述】:

所以我仍在努力达到那个“啊哈!”聚合和聚合根的时刻(什么是新的,对吗?),我看到 Martin Fowler 说过以下内容:

聚合将使其组件对象之一作为聚合根。来自聚合外部的任何引用都应该只转到 聚合根。因此,根可以确保数据的完整性 聚合为一个整体。

因此,我正在阅读此内容,因为聚合是父对象,并且聚合中的对象之一被选为聚合根。此聚合之外的任何内容都只能包含对根的引用。这让我很困惑。如果根是聚合的part,它怎么能保证什么?

据我了解,聚合的全部目的是 a) 作为域对象(值对象和其他聚合)的逻辑分组,b) 作为与聚合的所有交互必须发生的事务边界,并且该回购仅处理聚合。但是假设我有这样的聚合:

public class UserInventory
{
        private List<InventoryItem> _inventoryItems;

        // the aggregate root
        public User User { get; }       

        public ReadOnlyCollection<InventoryItem> Inventory => _inventoryItems;

        public UserInventory(User root, IEnumerable<InventoryItem> inventory)
        {
            User = root;
            _inventoryItems = inventory;
        }

        public void UpdateItemDescription(Guid itemId, ItemDescription newDescription)
        {
            _inventoryItems.Single(i => i.Id == itemId).Description = newDescription;
            DomainEvents.Notify(new InventoryItemUpdated(User));
        }
}

现在我的应用层想要更改特定库存项目的描述,因此,由于它不应该直接与库存项目对话,我公开UpdateItemDescription(Guid, ItemDescription) 来控制这个过程,因为这将是聚合的机会强制执行任何不变量(为了清楚起见,我省略了)

我觉得这是一个不错的聚合示例,但我不明白聚合根如何在这里“确保完整性”。我查看了 C# DDD 示例应用程序,但找不到任何明显的问题答案,但请随时在您的答案中引用它。

谁能澄清一下?我是否遗漏了什么或误解了什么?

【问题讨论】:

  • 虽然 DDD 一开始就像是设计和编码的混合体,但如今它是一种应用程序设计方法(识别领域模型),但它保留了旧名称,例如 value object 或聚合根。问题是,现在许多开发人员都在使用函数式方法,他们没有很多对象,但是他们在做 DDD 并且他们实现聚合根作为函数。可以说 AR 是负责执行聚合一致性规则的聚合的门面,不管它是如何实现的。

标签: c# domain-driven-design


【解决方案1】:

它与encapsulation(一个基本的 OOP 原则)有关。让我们花点时间看看这个公共合约:

public class UserInventory
{
        public User User { get; }       
        public ReadOnlyCollection<InventoryItem> Inventory => _inventoryItems;
} 

你在这里所做的是引入两个弱点。第一个是从另一个聚合根中暴露一个聚合根。这意味着有人可以这样做:

inventory.User.FirstName = "Arne";
_inventoryRepository.Update(inventory);

这违反了Law Of Demeter

该代码适用于谁?可能不是库存存储库的责任是持久化库存对象。其他一切都会导致数据层(即每个存储库都需要能够调用所有其他存储库等)和业务层(什么时候允许/有效的更改?)

所以从引用其他根聚合的 id 开始:

public class UserInventory
{
        public int UserId { get; }       
        public ReadOnlyCollection<InventoryItem> Inventory => _inventoryItems;
} 

下一个问题是你暴露了InventoryItem 列表。这就是 DDD 书所谈论的内容。 Inventory 类无法控制它的聚合。假设您的Inventory 中有一个TotalValue 属性:

public class UserInventory
{
        public int UserId { get; }       
        public ReadOnlyCollection<InventoryItem> Inventory => _inventoryItems;
        public decimal TotalValue {get; set; }
} 

如果有人直接调整库存商品的价格会怎样?

inventory.FirstOrDefault(x=>x.Name = XX).Value = 456.32;

总数会反映正确的值吗?不,因为您没有保护聚合。

正确的设计就是不暴露物品:

public class UserInventory
{
        private List<InventoryItem> _inventoryItems;

        public UserInventory(User root, IEnumerable<InventoryItem> inventory)
        {
            User = root;
            _inventoryItems = inventory;
        }

        public int UserId { get; }       

        public void UpdateItemDescription(Guid itemId, ItemDescription newDescription)
        {
            _inventoryItems.Single(i => i.Id == itemId).Description = newDescription;
            DomainEvents.Notify(new InventoryItemUpdated(User));
        }
}

现在你问。 如果我要到处进行该死的适当封装,我怎么能向用户显示东西?

将写入模型与读取模型分开。当您从存储库中查询内容以显示信息时,您可以返回 DTO。它们不包含任何方法,仅代表状态。

当需要做一些工作时,应用程序服务可以使用存储库来获取真实对象,对其进行操作,然后将其持久化。

那么在本例中,UserInventory 是一个域对象,被选为 UserInventory-User-Inventory 聚合的根?并且所有不是根的聚合成员的域对象都作为私有成员保存在根中,通过根上的显式行为公开访问?你能推荐一些关于如何从根源中获取状态的好的模式或例子吗?

当转向正确设计的 DDD 时,大多数人看到的是 CQRS 非常适合。因为在 CQRS 中,您可以清楚地区分读取的内容和写入的内容。您用于读取的任何内容(即为用户显示信息)都不会在写入站点中重用。驱动写入端的是基于任务的操作。即,不是说将字段 A、B、c 更新为这些值,命令更多地关注业务操作使用信息 A 和 B 完成订单。区别非常重要,因为底层域模型结构根本不受命令的影响。我的意思是您的域没有在写入模型中的任何地方公开。所有的变化都是通过命令来驱动的。

这也意味着读取端可以包含满足特定用例的专用对象。我个人创建的对象可以解决 UI 中的特定需求。我的订单实体可能由OrderListDTO 表示,每行有一组最少的属性可以显示概览,而“OrderDetailsDTO”表示订单的细节。

如果您还没有准备好应对这种变化(使用 CQRS 保护您的域),您可以通过为读写模型创建专门的应用程序服务来专门化您的应用程序。

重要的转变是要认识到订单的表示方式不是单一的,而是根据订单在顶层的消费方式而有所不同。

【讨论】:

  • 值得一提的是,这适用于完整的 OOP 实现。如果你去功能化,实现会有所不同,但聚合和它的根仍然是相同的。区分抽象(聚合的 DDD 概念以及实体、VO、AR 等)及其实现(即实际代码)非常重要。
  • 我明白了。那么在这个例子中,UserInventory 是一个域对象,被选为 UserInventory-User-Inventory 聚合的根?并且所有不是根的聚合成员的域对象都作为私有成员保存在根中,通过根上的显式行为公开访问?你能推荐一些关于如何从根源中获取状态的好的模式或例子吗?
  • 另外 +1 表示“现在你要问了。如果我要到处进行该死的适当封装,我怎么能向用户显示东西?”因为这正是我的想法哈哈
  • 阅读我的更新。你是怎么理解我的一些胡言乱语的:)
  • @Sinaesthetic 需要更多信息吗?
【解决方案2】:

聚合将使其组件对象之一作为聚合根。来自聚合外部的任何引用都应该只转到聚合根。因此,根可以确保聚合作为一个整体的完整性。

所以我正在阅读这个,因为聚合是父对象

这不太对。正如埃文斯所描述的,“聚合体”不是一个对象。将其视为域的子集更为准确,由某些状态以及与该状态交互的所有业务规则组成。

关键思想是边界——如果您需要更改聚合内的数据,那么确保更改完整性所需的一切也都在边界内。同样,如果您需要更改聚合外的数据,则不需要聚合内的任何状态。

正如 jgauffin 所说,“封装”。

如果根是聚合的一部分,它怎么能确保什么?

因为“告诉,不要问”

聚合根是聚合中唯一暴露的实体。它实际上是聚合中对象图的根;聚合中的每个状态都可以通过从聚合根遍历到它来到达。

聚合中的每个数据突变都需要在聚合根上执行命令。如果命令会产生无效状态,则根负责拒绝该命令。所以规则可以被编码到根对象本身,或者它们可以委托给同一聚合中的其他对象。

通常的风格是聚合中的实体负责自己的状态,父实体负责执行子实体之间的协调。但这只是实现细节——聚合只包含一个负责所有事情的根实体并不罕见。

简而言之,当您加载“聚合”时,您正在加载整个对象图——而不仅仅是根。聚合以有效状态加载——满足所有规则。更改聚合状态的唯一方法是运行根实体公开的命令;根实体负责拒绝任何违反完整性规则的命令。

【讨论】:

  • +1 表示聚合不是对象而是域的子集。似乎很少有开发人员明白这一点。
  • 是的,这是非常重要的一点,我想这是我第一次看到有人指出这一点。措辞具有误导性。所以我们很清楚,聚合不是我可以在解决方案中指出的东西吗?这是我必须描述/记录的内容,例如“此帐户对象是帐户-用户-商家-客户聚合的聚合根”。例如?
猜你喜欢
  • 2014-04-13
  • 1970-01-01
  • 1970-01-01
  • 2014-10-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-02-04
  • 1970-01-01
相关资源
最近更新 更多