views:

499

answers:

3

If I have an Order class an as aggregate root and 1000 line items.

How do I load just one of the 1000 line items? As far as I understand, a line item can only be accessed through the Order class and has a "local" identity. Would I still create a repository method at the OrderRepository like "GetLineItemById"?

Edit to comment the answer: Currently I don't think it's reasonable to have an immutable children. What if I have an Customer class with several addresses, contracts and even more child collections. A huge entity I want to perform CRUD methods on.

I would have

public class Customer
{
    public IEnumerable<Address> Addresses { get; private set; }
    public IEnumerable<Contracts> Contracts { get; private set; }
    ...
}

Would I have to do something like this if a user corrects the street of an address or a property of a contract?

public class Customer
{
    public void SetStreetOfAddress(Address address, street){}

    public void SetStreetNumberOfAddress(Address address, streetNumber){}
}

The customer class would be full of child manipulation methods then. So I would rather do

addressInstance.Street = "someStreet";

I think I am misunderstanding the whole concept.. :)

+2  A: 

1) 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.

2) 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).

3) 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 to Contract and Address aggregates.

Jeff Sternal
But when e.g. using Nhibnerate with lazy loading the LineItems collection, Order.LineItems[id] would cause the collection to be fully loaded. Anyway Order.LineItems[id] would require line items to be some sort of a dictionary. So how would you data-access-wise access a single child of a collection then without loading 1000 items first?
Chris
One can't be dogmatic about this - if there's a proven performance problem, all bets are off and you can do what you need to fix things; in those cases, there's no reason to be a DDD purist. On the other hand, don't assume that loading 1000 items is going to hurt performance significantly: if your database is well-designed and you have a reasonable load, it shouldn't be a problem. After all, you're not writing Twitter or you wouldn't be using NHibernate!
Jeff Sternal
If NHibernate is anything at all like Hibernate you can make it use a different fetch strategy for a certain collections. E.g batch fetch 10 LineItems at a time
Konstantin
A: 

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.

Paco
IMHO, I do not compose aggregates inside of other aggregates. To me that defeats the idea of bounded contexts, and the "root" metaphor. Instead of composing aggregate B in aggregate A, I merely expose the identity of aggregate B in aggregate A. This separates the aggregates and leaves the repository-aggregate pattern intact. It also better follows the Law of Demeter.
gWiz
@gWiz: Nesting aggregates does not makes it impossible to use the law of Demeter. You can create methods that change the child aggregate root state in the parent aggregate root as well when the methods are used in the context of the parent object. Some methods might only be used in the child object, than you can define them there. I don't mean to always nest aggregates. Sometimes you do nest them, most you don't.
Paco
Ya, I gotcha. I just thought the rationale against it needed to be fleshed out. But I suppose there are always exceptions to the rules.
gWiz
+3  A: 

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.

gWiz