聚合根可以引用另一个根吗?

Ish*_*mas 5 c# architecture design-patterns domain-driven-design aggregateroot

我有点困惑。我只是看着
朱莉·勒曼对DDD Pluralsight视频和这里的混乱,我有:有一个简单的在线商店例如: 采购订单项目供应商,有什么这里的总根源在哪里?

从技术上说是采购订单,对不对?它是针对特定供应商的,上面有物品。那讲得通。

但是..该物品也是聚合根吗?它具有其他“子对象”,例如“品牌”,“设计师”,“颜色”,“类型”等。您可能在SOA系统中有一个单独的应用程序来编辑和管理项目(无PO)。因此,在这种情况下,您将必须访问聚合根目录的组件-这是不允许的。

在此示例中,项目是否是聚合根?

Mil*_*idi 6

虽然这个问题有一个公认的答案,但阅读这篇文章可能会对这个问题的其他读者有所帮助。
根据这篇文章,不是直接引用另一个聚合,而是创建一个值对象来包装ID聚合根的 并将其用作引用。这使得维护聚合一致性边界变得更容易,因为您甚至不会意外地从另一个聚合中更改一个聚合的状态。它还可以防止在检索聚合时从数据存储中检索深层对象树


Rea*_*ria 5

这取决于您所处的上下文。我将尝试用一些不同的上下文示例进行解释,并在最后回答问题。

假设第一个上下文是关于向系统添加新项目的。在这种情况下,项是聚合根。您很可能正在构造新项目并将其添加到数据存储或删除项目。假设该类可能如下所示:

namespace ItemManagement
{
    public class Item : IAggregateRoot // For clarity
    {
        public int ItemId {get; private set;}

        public string Description {get; private set;}

        public decimal Price {get; private set;}

        public Color Color {get; private set;}

        public Brand Brand {get; private set;} // In this context, Brand is an entity and not a root

        public void ChangeColor(Color newColor){//...}

        // More logic relevant to the management of Items.
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,假设系统的不同部分通过添加和删除订单中的项目来构成采购订单。在这种情况下,Item不仅不是聚合根,而且理想情况下甚至不是同一类。为什么?因为在这种情况下,品牌,颜色和所有逻辑很可能完全不相关。这是一些示例代码:

namespace Sales
{
    public class PurchaseOrder : IAggregateRoot
    {
        public int PurchaseOrderId {get; private set;}

        public IList<int> Items {get; private set;} //Item ids

        public void RemoveItem(int itemIdToRemove)
        {
            // Remove by id
        }

        public void AddItem(int itemId) // Received from UI for example
        {
            // Add id to set
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,项仅由ID表示。这是此上下文中唯一相关的部分。我们需要知道采购订单上有哪些项目。我们不在乎品牌或其他任何东西。现在您可能想知道您如何知道采购订单上物品的价格和描述?这是另一种上下文-查看和删除项目,类似于网络上的许多“结帐”系统。在这种情况下,我们可能具有以下类别:

namespace Checkout
{
    public class Item : IEntity
    {
        public int ItemId {get; private set;}

        public string Description {get; private set;}

        public decimal Price {get; private set;}
    }

    public class PurchaseOrder : IAggregateRoot
    {
        public int PurchaseOrderId {get; private set;}

        public IList<Item> Items {get; private set;}

        public decimal TotalCost => this.Items.Sum(i => i.Price);

        public void RemoveItem(int itemId)
        {
            // Remove item by id
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,我们有一个非常瘦的item版本,因为此上下文不允许更改Items。它仅允许查看采购订单和删除项目的选项。用户可以选择一个要查看的项目,在这种情况下,上下文将再次切换,并且您可以将整个项目作为聚合根加载,以显示所有相关信息。

在确定您是否有存货的情况下,我认为这是具有不同根源的另一种情况。例如:

namespace warehousing
{
    public class Warehouse : IAggregateRoot
    {
        // Id, name, etc

        public IDictionary<int, int> ItemStock {get; private set;} // First int is item Id, second int is stock

        public bool IsInStock(int itemId)
        {
            // Check dictionary to see if stock is greater than zero
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

每个上下文都通过其自己的根和实体版本来公开其执行职责所需的信息和逻辑。一无所有,一无所有。

我知道您的实际应用程序将变得更加复杂,需要在添加项目到PO等之前进行库存检查。要点是,理想情况下,您的根目录应该已经加载了完成功能所需的所有内容,并且不应有其他上下文在不同的上下文中影响根的设置。

因此,回答您的问题-根据上下文,任何类都可以是实体或根,如果您对边界上下文进行了很好的管理,则根几乎不必互相引用。您不必在所有上下文中都重复使用同一类。实际上,使用同一类通常会导致诸如User类之类的行长达3000行,因为User类具有管理银行帐户,地址,个人资料详细信息,朋友,受益人,投资等的逻辑。所有这些都不属于同一类。

回答您的问题

  1. 问:为什么将物料AR称为ItemManagement,而将PO AR称为PurchaseOrder?

命名空间名称反映了您所处上下文的名称。因此,在项目管理的上下文中,Item是根,并将其放置在ItemManagement命名空间中。您还可以将ItemManagement视为聚合,并将Item视为此聚合的。我不确定这是否能回答您的问题。

  1. 问:实体(如轻型物品)也应具有方法和逻辑吗?

这完全取决于您所处的环境。如果要仅使用物料来显示价格和名称,则不会。如果不应在上下文中使用逻辑,则不应公开。在Checkout上下文示例中,Item没有逻辑,因为它们仅用于向用户显示采购订单的组成。如果存在其他功能,例如,用户可以在结帐期间更改采购订单上某项商品的颜色(例如电话),则可以考虑在这种情况下在该商品上添加这种类型的逻辑。

  1. AR如何访问数据库?他们是否应该有一个接口,比如说IPurchaseOrderData,使用void RemoveItem(int itemId)之类的方法?

我道歉。我假设您的系统正在使用某种ORM,例如(N)Hibernate或Entity Framework。在这种ORM的情况下,ORM将足够聪明,可以在持久化根(假设映射配置正确)时自动将集合更新转换为正确的sql。在您自己管理持久性的情况下,它会稍微复杂一些。要直接回答该问题-您可以将数据存储区接口注入根目录,但我建议不要这样做。

您可能有一个可以加载和保存聚合的存储库。让我们以在CheckOut上下文中具有物料的采购订单示例为例。您的存储库可能具有以下内容:

public class PurchaseOrderRepository
{
    // ...
    public void Save(PurchaseOrder toSave)
    {
        var queryBuilder = new StringBuilder();

        foreach(var item in toSave.Items)
        {
           // Insert, update or remove the item
           // Build up your db command here for example:
           queryBuilder.AppendLine($"INSERT INTO [PurchaseOrder_Item] VALUES ([{toSave.PurchaseOrderId}], [{item.ItemId}])");

        }
    }
    // ...
}
Run Code Online (Sandbox Code Playgroud)

您的API或服务层将如下所示:

public void RemoveItem(int purchaseOrderId, int itemId)
{
    using(var unitOfWork = this.purchaseOrderRepository.BeginUnitOfWork())
    {
        var purchaseOrder = this.purchaseOrderRepository.LoadById(purchaseOrderId);

        purchaseOrder.RemoveItem(itemId);

        this.purchaseOrderRepository.Save(purchaseOrder); 

        unitOfWork.Commit();
    }
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,您的存储库可能变得很难实施。实际上,使它删除采购订单上的项目并重新添加PurchaseOrder根目录上的项目可能更容易(很简单,但不建议这样做)。每个聚合根您将有一个存储库。

主题外: 像(N)Hibernate这样的ORM将通过跟踪自加载根以来对根所做的所有更改来处理Save(PO)。因此,它将具有更改的内部历史记录,并在发出保存命令时发出适当的命令以使数据库状态与根状态保持同步,以发出对根及其子级所做的每个更改的地址。