这取决于你所处的上下文。我将尝试用几个不同的上下文例子来解释,并在最后回答问题。
假设第一个上下文是关于向系统添加新项目。在这种情况下,Item 是聚合根。您很可能会构建和添加新项目到您的数据存储或删除项目。让我们假设该类可能如下所示:
namespace ItemManagement
{
public class Item : IAggregateRoot
{
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;}
public void ChangeColor(Color newColor){
}
}
现在假设系统的不同部分允许通过向订单中添加或删除项目来组成采购订单。在这种情况下,Item并不是聚合根,而且最好甚至不是相同的类。为什么呢?因为品牌、颜色和所有逻辑在这种情况下很可能完全不相关。以下是一些示例代码:
namespace Sales
{
public class PurchaseOrder : IAggregateRoot
{
public int PurchaseOrderId {get; private set;}
public IList<int> Items {get; private set;}
public void RemoveItem(int itemIdToRemove)
{
}
public void AddItem(int itemId)
{
}
}
}
在这个上下文中,Item 只由 Id 表示。这是此上下文中唯一相关的部分。我们需要知道采购订单上有哪些物品,不关心品牌或其他任何信息。现在你可能想知道如何了解采购订单上物品的价格和描述?这是另一个上下文——查看和删除物品,类似于Web上的许多“结账”系统。在这种情况下,我们可能有以下类:
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)
{
}
}
}
在这种情况下,我们有一个非常简化的item版本,因为这个上下文不允许修改物品。它仅允许查看采购订单并选择删除物品的选项。如果用户选择查看一个Item,则上下文会再次切换,并且您可以将完整的item作为聚合根加载,以显示所有相关信息。
当确定是否有库存时,我认为这是另一个具有不同聚合根的上下文。例如:
namespace warehousing
{
public class Warehouse : IAggregateRoot
{
public IDictionary<int, int> ItemStock {get; private set;}
public bool IsInStock(int itemId)
{
}
}
}
每个上下文通过其自己版本的根和实体,公开执行其职责所需的信息和逻辑。无多余也无不足。
我知道你的实际应用程序会更加复杂,在将项目添加到采购订单之前需要进行库存检查等。重点是,你的根理想要已经加载完成所需的一切,而且没有其他上下文会影响在不同上下文中根的设置。
因此,回答你的问题——任何类都可以是实体或根,具体取决于上下文,如果你管理了好领域边界,那么你的根很少需要相互引用。你不必在所有上下文中重复使用相同的类。事实上,如果使用相同的类,通常会导致诸如User类长度为3000行,因为它具有管理银行账户、地址、个人资料详情、朋友、受益人、投资等逻辑。这些东西都不应该放在一起。
回答你的问题
- Q:为什么Item AR被称为ItemManagement,而PO AR仅被称为PurchaseOrder?
命名空间名称反映您所在的上下文名称。因此,在项目管理的上下文中,Item是根并且放置在ItemManagement命名空间中。你也可以将ItemManagement视为聚合,Item作为该聚合的根。我不确定这是否回答了你的问题。
- Q:实体(如轻型Item)是否也应该具有方法和逻辑?
这完全取决于你上下文的内容。如果只用Item来显示价格和名称,则不需要公开逻辑,除非它确实在上下文中使用。例如,在CheckOut上下文示例中,Item没有任何逻辑,因为它们只是用于显示用户的购买订单包含哪些物品。如果有不同的功能,例如用户可以在结账时更改购买订单中物品的颜色(例如手机),则可能会考虑在该上下文中在物品上添加此类型的逻辑。
- ARs如何访问数据库?它们应该有接口,例如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)
{
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将通过跟踪加载后根对象所做的所有更改来处理Save(PO)。因此,当你保存时,它将具有内部历史记录,记录根及其子项所做的每个更改,并发出适当的命令以使数据库状态与根状态同步,通过发出SQL语句来解决每个更改。