域驱动设计:如何访问聚合根的子节点
Posted
技术标签:
【中文标题】域驱动设计:如何访问聚合根的子节点【英文标题】:Domain Driven Design: How to access child of aggregate root 【发布时间】:2011-01-07 10:25:55 【问题描述】:如果我有一个 Order 类作为聚合根和 1000 个订单项。
如何仅加载 1000 个订单项中的一个?据我了解,订单项只能通过 Order 类访问,并且具有“本地”身份。我是否仍会在 OrderRepository 中创建一个存储库方法,例如“GetLineItemById”?
编辑以评论答案: 目前我认为有一个不可变的孩子是不合理的。如果我有一个包含多个地址、合同甚至更多子集合的 Customer 类怎么办。我想对其执行 CRUD 方法的巨大实体。
我会的
public class Customer
public IEnumerable<Address> Addresses get; private set;
public IEnumerable<Contracts> Contracts get; private set;
...
如果用户更正地址的街道或合同的财产,我是否必须这样做?
public class Customer
public void SetStreetOfAddress(Address address, street)
public void SetStreetNumberOfAddress(Address address, streetNumber)
那时客户类将充满子操作方法。所以我宁愿做
addressInstance.Street = "someStreet";
我想我误解了整个概念.. :)
【问题讨论】:
【参考方案1】:-
通过简单的只读属性或 get 方法访问聚合根的子级并没有错。
重要的是确保与子节点的所有交互都由聚合根进行调解,以便有一个单一的、可预测的位置来保证不变量。
所以Order.LineItems
很好,只要它返回(公开)不可变对象的不可变集合。同样Order.LineItems[id]
。有关示例,请参见 the source for the canonical Evans-approved ddd example,其中聚合根 Cargo
类公开了它的几个子实体,但子实体是不可变的。
-
聚合根可以保存对其他聚合根的引用,但它们不能相互更改。
如果您有“蓝皮书” (Domain-Driven Design),请参阅第 127 页上的示例,该示例显示了您可能如何拥有Car.Engine
,其中Car
和Engine
都是聚合根,而是一个引擎不是汽车聚合的一部分,您不能使用Car
的任何方法更改引擎(反之亦然)。
-
在域驱动设计中,您不必使所有类都聚合根或聚合的子级。您只需要聚合根来封装一组有凝聚力的类之间的复杂交互。您提出的
Customer
类听起来根本不应该是聚合根 - 只是一个包含对 Contract
和 Address
聚合的引用的常规类。
【讨论】:
但是当例如使用 Nhibnerate 和延迟加载 LineItems 集合,Order.LineItems[id] 将导致集合完全加载。无论如何 Order.LineItems[id] 将要求行项目是某种字典。那么,如何在不先加载 1000 个项目的情况下以数据访问方式访问集合的单个子项? 对此不能教条主义 - 如果存在已证实的性能问题,那么所有的赌注都没有了,你可以做你需要做的事情来解决问题;在这些情况下,没有理由成为 DDD 纯粹主义者。另一方面,不要假设加载 1000 个项目会显着损害性能:如果您的数据库设计良好并且负载合理,那么这应该不是问题。毕竟,你不是在写 Twitter,否则你就不会使用 NHibernate! 如果 NHibernate 与 Hibernate 完全一样,你可以让它对某些集合使用不同的获取策略。例如,一次批量获取 10 个 LineItems 我对此有一个大问题。要将子行为放在根中,您必须在根上使用一种方法,在子上使用另一种方法。您如何保护子方法免受不必要的使用?如果你把它做成内部的,它可以被同一个程序集中的其他任何东西使用吗?你如何强制调用通过根目录? 我读了蓝皮书。但我的印象是每个类(或更好的实例)都必须是一个聚合。您从哪里读到可能存在任何聚合之外的类/实例?在我看来,这会使它们聚合并聚合根。【参考方案2】:当您在“我如何加载 1000 个订单项中的一个?”中说加载时你的意思是“从数据库加载”?换句话说,我如何从数据库中只加载聚合根的一个子实体?
这有点复杂,但您可以让存储库返回聚合根的派生,其字段是延迟加载的。例如
namespace Domain
public class LineItem
public int Id get; set;
// stuff
public class Order
public int Id get; set;
protected ReadOnlyCollection<LineItem> LineItemsField;
public ReadOnlyCollection<LineItem> LineItems get; protected set;
public interface IOrderRepository
Order Get(int id);
namespace Repositories
// Concrete order repository
public class OrderRepository : IOrderRepository
public Order Get(int id)
Func<IEnumerable<LineItem>> getAllFunc = () =>
Collection<LineItem> coll;
// logic to build all objects from database
return coll;
;
Func<int, LineItem> getSingleFunc = idParam =>
LineItem ent;
// logic to build object with 'id' from database
return ent;
;
// ** return internal lazy-loading derived type **
return new LazyLoadedOrder(getAllFunc, getSingleFunc);
// lazy-loading internal derivative of Order, that sets LineItemsField
// to a ReadOnlyCollection constructed with a lazy-loading list.
internal class LazyLoadedOrder : Order
public LazyLoadedOrder(
Func<IEnumerable<LineItem>> getAllFunc,
Func<int, LineItem> getSingleFunc)
LineItemsField =
new ReadOnlyCollection<LineItem>(
new LazyLoadedReadOnlyLineItemList(getAllFunc, getSingleFunc));
// lazy-loading backing store for LazyLoadedOrder.LineItems
internal class LazyLoadedReadOnlyLineItemList : IList<LineItem>
private readonly Func<IEnumerable<LineItem>> _getAllFunc;
private readonly Func<int, LineItem> _getSingleFunc;
public LazyLoadedReadOnlyLineItemList(
Func<IEnumerable<LineItem>> getAllFunc,
Func<int, LineItem> getSingleFunc)
_getAllFunc = getAllFunc;
_getSingleFunc = getSingleFunc;
private List<LineItem> _backingStore;
private List<LineItem> GetBackingStore()
if (_backingStore == null)
_backingStore = _getAllFunc().ToList(); // ** lazy-load all **
return _backingStore;
public LineItem this[int index]
get
if (_backingStore == null) // bypass GetBackingStore
return _getSingleFunc(index); // ** lazy-load only one from DB **
return _backingStore[index];
set throw new NotSupportedException();
// "getter" implementations that use lazy-loading
public IEnumerator<LineItem> GetEnumerator() return GetBackingStore().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() return GetEnumerator();
public bool Contains(LineItem item) return GetBackingStore().Contains(item);
public void CopyTo(LineItem[] array, int arrayIndex) GetBackingStore().CopyTo(array, arrayIndex);
public int Count get return GetBackingStore().Count;
public bool IsReadOnly get return true;
public int IndexOf(LineItem item) return GetBackingStore().IndexOf(item);
// "setter" implementations are not supported on readonly collection
public void Add(LineItem item) throw new NotSupportedException("Read-Only");
public void Clear() throw new NotSupportedException("Read-Only");
public bool Remove(LineItem item) throw new NotSupportedException("Read-Only");
public void Insert(int index, LineItem item) throw new NotSupportedException("Read-Only");
public void RemoveAt(int index) throw new NotSupportedException("Read-Only");
OrderRepository.Get(int)
的调用者会收到实际上只是一个 Order 对象的东西,但实际上是一个 LazyLoadedOrder。当然,要做到这一点,您的聚合根必须提供一两个虚拟成员,并围绕这些扩展点进行设计。
编辑以解决问题更新
在地址的情况下,我会将其视为值对象,即数据的不可变组合一起被视为单个值。
public class Address
public Address(string street, string city)
Street = street;
City = city;
public string Street get; private set;
public string City get; private set;
然后,为了修改聚合,您创建了一个新的 Address 实例。这类似于 DateTime 的行为。您还可以向 Address 添加方法方法,例如 SetStreet(string)
,但这些应该返回 Address 的新实例,就像 DateTime 的方法返回 DateTime 的新实例一样。
在您的情况下,不可变的 Address 值对象必须与 Addresses 集合的某种观察相结合。一种简单明了的技术是在单独的集合中跟踪添加和删除的 AddressValues。
public class Customer
public IEnumerable<Address> Addresses get; private set;
// backed by Collection<Address>
public IEnumerable<Address> AddedAddresses get; private set;
// backed by Collection<Address>
public IEnumerable<Address> RemovedAddresses get; private set;
public void AddAddress(Address address)
// validation, security, etc
AddedAddresses.Add(address);
public void RemoveAddress(Address address)
// validation, security, etc
RemovedAddresses.Add(address);
// call this to "update" an address
public void Replace(Address remove, Address add)
RemovedAddresses.Add(remove);
AddedAddresses.Add(add);
或者,您可以使用ObservableCollection<Address>
支持地址。
这确实是一个纯粹的 DDD 解决方案,但你提到了 NHibernate。我不是 NHibernate 专家,但我想您必须添加一些代码才能让 NHibernate 知道地址更改的存储位置。
【讨论】:
【参考方案3】:当您需要通过 Id 访问子实体时,使子实体本身成为聚合根。聚合根具有其他聚合根作为子级,甚至子级引用父级都没有错。子实体的单独存储库是可以的。当聚合根包含聚合根时,我们必须牢记“有界上下文”的概念,以防止将域的太大部分耦合在一起并使代码难以更改。发生这种情况时,原因是大多数情况下聚合根嵌套得很深。在您的情况下,这应该不是问题,按顺序嵌套 lineitems 听起来非常合理。
要回答是否应该嵌套订单项的问题,我现在知道为什么要按 id 加载订单项,并且每个订单销售 1000 个项目听起来应用程序会卖很多?
当您在订单中嵌套订单项,并且您希望订单有很多订单项时,您可以查看几个映射/缓存/查询加载选项,以使大订单按照应用程序的需要执行.如何以最快的方式加载您需要的订单项的答案取决于您使用它的上下文。
【讨论】:
恕我直言,我不在其他聚合中编写聚合。对我来说,这违背了有界上下文和“根”隐喻的想法。我没有在聚合 A 中组合聚合 B,而只是在聚合 A 中公开聚合 B 的身份。这将聚合分离并保持存储库-聚合模式完好无损。它也更好地遵循得墨忒耳法则。 @gWiz:嵌套聚合并不会导致无法使用得墨忒耳定律。当在父对象的上下文中使用这些方法时,您也可以在父聚合根中创建更改子聚合根状态的方法。有些方法可能只在子对象中使用,而不是您可以在那里定义它们。我并不是说总是嵌套聚合。有时你会嵌套它们,但大多数情况下你不会。 是的,我明白了。我只是认为需要充实反对它的理由。但我想规则总有例外。以上是关于域驱动设计:如何访问聚合根的子节点的主要内容,如果未能解决你的问题,请参考以下文章
基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践和原则