Domain Driven Design: How to access child of aggregate root
When you need to access the child entity by Id, makes the child entity an aggregate root itself. There is nothing wrong with aggregate roots having other aggregate roots as children, or even with children with a reference to the parent. A separate repository for the child entity is all right. When aggregate roots hold aggregate roots, we have to keep the concept of "bounded contexts" in mind to prevent coupling too big parts of the domain together and make the code hard to change. When this happens, the reason is most of the time that aggregate roots get nested to deep. This should not be a problem in your case, the nesting of lineitems in an order sounds very reasonable.
To answer the question if you should nest the line items, I have the now why you want to load the line items by id, and selling 1000 items per order sounds like the application will be selling a lot?
When you nest the line items in the order, and you expect orders to have a lot of line items, you can look at several mapping/caching/query-loading options to make the big orders perform like is needed by the application. The answer how to load the line items you need the fastest way, depends on the context you use it in.
When you say load in "How do I load just one of the 1000 line items?" do you mean "load from the database"? In other words, how do I load just one child entity of an aggregate root from the database?
This is a bit complex, but you can have your repositories return a derivation of the aggregate root, whose fields are lazy-loaded. E.g.
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"); }
}
}
Callers of OrderRepository.Get(int)
would receive something that is effectively just an Order object, but is actually a LazyLoadedOrder. Of course to do this your aggregate roots must provide a virtual member or two, and be designed around those extension points.
Edit to address question updates
In the case of an address, I would treat it as a value object, i.e. immutable compositions of data that are together treated as a single value.
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;}
}
Then, in order to modify the aggregate, you create a new instance of Address. This is analogous to the behavior of DateTime. You can also add methods methods to Address such as SetStreet(string)
but these should return new instances of Address, just as the methods of DateTime return new instances of DateTime.
In your case, immutable Address value objects have to be coupled with some kind of observation of the Addresses collection. A straightforward and clean technique is to track added and removed AddressValues in separate collections.
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);
}
}
Alternatively you could back Addresses with an ObservableCollection<Address>
.
This is indeed a pure DDD solution, but you mentioned NHibernate. I'm not an NHibernate expert, but I imagine you will have to add some code to let NHibernate know where changes to Addresses are being stored.
- There's nothing wrong with accessing children of an aggregate root via simple, read-only properties or get methods.
The important thing is to make sure that all interactions with children are mediated by the aggregate root so that there's a single, predictable place to guarantee invariants.
So Order.LineItems
is fine, as long as it returns an immutable collection of (publicly) immutable objects. Likewise Order.LineItems[id]
. For an example see the source for the canonical Evans-approved ddd example, where the aggregate root Cargo
class exposes several of its children, but the child entites are immutable.
- Aggregate roots can hold references to other aggregate roots, they just can't change each other.
If you have "the blue book" (Domain-Driven Design), see the example on page 127, which shows how you might have Car.Engine
, where both Car
and Engine
are aggregate roots, but an engine isn't part of a car's aggregate and you can't make changes to an engine using any of the methods of Car
(or vice-versa).
- In domain-driven design, you don't have to make all your classes aggregate roots or children of aggregates. You only need aggregate roots to encapsulate complex interactions among a cohesive group of classes. The
Customer
class you proposed sounds like it shouldn't be an aggregate root at all - just a regular class that holds references toContract
andAddress
aggregates.