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

时间:2018-05-30 05:49:12

标签: c# design-patterns architecture domain-driven-design aggregateroot

我有点困惑。我刚观看了 Julie Lerman关于DDD的Pluralsight视频,这里有我的困惑: 有一个简单的在线商店示例: 采购订单 供应商项目,这里的聚合根是什么?

技术上采购订单,对吗?它适用于特定的供应商,并且上面有。这是有道理的。

但是.. Item也是聚合根?它还有其他“子对象”,如“品牌”,“设计器”,“颜色”,“类型”等......您可能在SOA系统中有一个单独的应用程序来编辑和管理项目(没有PO)。那么......在这种情况下,您将不得不访问聚合根的组件 - 这是不允许的。

是否在此示例中聚合根?

3 个答案:

答案 0 :(得分:4)

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

让我们说第一个上下文是关于向系统添加新项目的全部内容。在此上下文中,Item是聚合根。您很可能正在构建和添加新项目到您的数据存储或删除项目。让我们说这个课程看起来如下:

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.
    }
}

现在让我们说系统的另一部分允许通过添加和删除订单中的商品来组成采购订单。在这种情况下,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
        }
    }
}

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

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
        }
    }
}

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

在确定您是否有股票的情况下,我认为这是另一个具有不同根的背景。例如:

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
        }
    }
}

每个上下文通过其自己的根和实体版本,公开了履行职责所需的信息和逻辑。没有更多,也没有更少。

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

所以回答你的问题 - 根据上下文,任何一个类都可以是实体或根,如果你已经很好地管理了有限的上下文,你的根很少需要相互引用。您不必在所有上下文中重用相同的类。实际上,使用相同的类通常会导致类似于用户类的行长达3000行,因为它具有管理银行帐户,地址,个人资料详细信息,朋友,受益人,投资等的逻辑。这些都不属于一起。

回答您的问题

  1. 问:为什么Item AR被称为ItemManagement,但PO AR只被称为PurchaseOrder?
  2. 命名空间名称反映了您所在的上下文的名称。因此,在项目管理的上下文中,Item是根,它放在ItemManagement命名空间中。您还可以将ItemManagement视为 Aggregate ,将Item视为此聚合的 Root 。我不确定这是否能回答你的问题。

    1. 问:实体(如轻物品)是否也有方法和逻辑?
    2. 这完全取决于你的背景。如果您打算仅使用Item显示价格和名称,那么不。如果不应在上下文中使用逻辑,则不应暴露逻辑。在Checkout上下文示例中,Item没有逻辑,因为它们仅用于向用户显示采购订单的组成。如果存在不同的功能,例如,用户可以在结帐时更改采购订单上的项目(如电话)的颜色,则可以考虑在该上下文中的项目上添加此类逻辑。

      1. AR如何访问数据库?他们应该有一个接口..让我们说IPurchaseOrderData,使用像void RemoveItem(int itemId)这样的方法吗?
      2. 我道歉。我假设你的系统正在使用某种类似ORM的(N)Hibernate或Entity框架。在这种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}])");
        
                }
            }
            // ...
        }
        

        您的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();
            }
        }
        

        在这种情况下,您的存储库可能变得非常难以实现。实际上,它可能更容易删除采购订单上的项目并重新添加PurchaseOrder根目录上的项目(简单但不推荐)。 每个聚合根目录都有一个存储库。

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

答案 1 :(得分:1)

  

有一个简单的在线商店示例:带有供应商物品的采购订单,这里的聚合根是什么?

这取决于你如何建模,而这又取决于你如何看待信息随时间的变化。

例如,一种可能的模型是将所有这些实体放入一个聚合中。

更常见的是将每个采购订单与其他采购订单分开处理;在这种情况下,您可能会使每个订单成为聚合根。由于多个订单可能与同一供应商有关系,因此供应商也可能是一个集合。

项目不太清楚 - 订单中的条目可能是该订单的本地条目,因此您不太可能创建单独的一致性边界来管理它们。另一方面,产品/ skus可能会被多个订单重复使用,这再次表明它们是一个单独的聚合。

在这种情况下通常会发生的情况是,聚合不会相互包含引用,而是可以用来查找引用的键。

因此,我的采购订单(#12345)可能包含&#34; 2个产品单元(#67890)&#34;,但如果我想知道的含义,那么我有获取产品(#67890)并使用它来查找产品的其余数据。

  

如果我想拥有一些PO的逻辑,比如&#34;用我们库存的物品做一些事情&#34;我必须得到该PO上的所有项目并对它们调用IsInStock()方法。 IsInStock是该项目的公共方法,所以我想我并没有违反DDD原则。我呢?

简答:不。

更长的答案:您要非常小心的地方是,当您有两个必须始终保持一致的数据时。试图协调不同聚合中数据的语义变得非常混乱。

答案 2 :(得分:0)

尽管该问题的答案已被接受,但阅读this article可能会对其他读者有所帮助。
根据{{​​3}}, 创建一个值对象,以包装聚合根的ID并将其用作引用,而不是直接引用另一个聚合。这样可以更轻松地保持聚合一致性边界,因为您甚至不能不小心从另一个聚合状态中更改一个聚合状态。当检索到聚合时,它还防止深层对象树被从数据存储中检索。