如果聚合根“IN”,聚合根如何确保聚合的完整性?

时间:2016-03-18 01:40:49

标签: c# domain-driven-design

所以我仍然想要达到那个“啊哈!”聚合和聚合根的时刻(什么是新的,对吧?)我看到Martin Fowler说了以下内容:

  

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

所以我读这个,因为聚合是父对象,并且聚合中的一个对象被选为聚合根。此聚合之外的任何内容都只能包含对根的引用。这对我来说很困惑。如果根是聚合的部分,它如何确保任何事情?

我的理解是聚合的整个目的是: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示例应用程序,但我找不到任何明显的问题答案,但可以在答案中引用它。

有人可以澄清吗?我错过了什么或误解了什么吗?

2 个答案:

答案 0 :(得分:6)

它与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类无法控制它的聚合。假设您的TotalValue

中有Inventory个属性
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保护您的域),您可以通过为读写模型创建专门的应用程序服务来专门化您的应用程序。

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

答案 1 :(得分:1)

  

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

     

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

那不太对劲。如埃文斯所描述的&#34;聚合&#34;不是一个对象。将其视为域的子集(由某个状态组成)以及与该状态交互的所有业务规则更准确。

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

正如jgauffin所说,&#34;封装&#34;。

  

如果根是聚合的一部分,它如何确保任何事情?

因为&#34;告诉,不要问&#34;

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

聚合中的每个数据变异都需要在聚合根上执行命令。如果该命令将产生无效状态,则root负责拒绝该命令。因此规则可以编码到根对象本身,也可以委托给同一聚合中的其他对象。

通常的风格是聚合体内的实体负责自己的状态,父体实体负责加强儿童之间的协调。但这仅仅是实现细节 - 如果聚合只包含一个负责所有内容的单个根实体,那么这一点并不罕见。

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