Strengthening your domain: Encapsulated collections


Previous posts in this series:

One of the common themes throughout the DDD book is that much of the nuts and bolts of structural domain-driven design is just plain good use of object-oriented programming.  This is certainly true, but DDD adds some direction to OOP, along with roles, stereotypes and patterns.  Much of the direction for building entities at the class level can, and should, come from test-driven development.  TDD is a great tool for building OO systems, as we incrementally build our design with only the behavior that is needed to pass the test.  Our big challenge then is to write good tests.

To fully harness TDD, we need to be highly attuned to the design that comes out of our tests.  For example, suppose we have our traditional Customer and Order objects.  In our world, an Order has a Customer, and a Customer can have many Orders.  We have this directionality because we can navigate this relationship from both directions in our application.  In the last post, we worked to satisfy invariants to prevent an unsupported and nonsensical state for our objects.

We can start with a fairly simple test:

[Test]
public void Should_add_the_order_to_the_customers_order_lists_when_an_order_is_created()
{
    var customer = new Customer();
    var order = new Order(customer);

    customer.Orders.ShouldContain(order);
}

At first, this test does not compile, as Customer does not yet contain an Orders member.  To make this test compile (and subsequently fail), we add an Orders list to Customer:

public class Customer
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Province { get; set; }
    public List<Order> Orders { get; set; }

    public string GetFullName()
    {
        return LastName + ", " + FirstName;
    }
}

With the Orders now exposed on Customer, we can make our test pass from the Order constructor:

public class Order
{
    public Order(Customer customer)
    {
        Customer = customer;
        customer.Orders.Add(this);
    }

And all is well in our development world, right?  Not quite.  This design exposes quite a bit of functionality that I don’t think our domain experts need, or want.  The design above allows some very interesting and very wrong scenarios:

[Test]
public void Not_supported_situations()
{
    // Removing orders?
    var customer1 = new Customer();
    var order1 = new Order(customer1);

    customer1.Orders.Remove(order1);

    // Clearing orders?
    var customer2 = new Customer();
    var order2 = new Order(customer1);

    customer2.Orders.Clear();

    // Duplicate orders?
    var customer3 = new Customer();
    var customer4 = new Customer();
    var order3 = new Order(customer3);

    customer4.Orders.Add(order3);
}

With the API I just created, I allow a number of rather bizarre scenarios, most of which make absolutely no sense to the domain experts:

  • Clearing orders
  • Removing orders
  • Adding an order from one customer to another
  • Inserting orders
  • Re-arranging orders
  • Adding an order without the Order’s Customer property being correct

This is where we have to be a little more judicious in the API we expose for our system.  All of these scenarios are possible in the API we created, but now we have some confusion on whether we should support these scenarios or not.  If I’m working in a similar area of the system, and I see that I can do a Customer.Orders.Remove operation, it’s not immediately clear that this is a scenario not actually coded for.  Worse, I don’t have the ability to correctly handle these situations if the collection is exposed directly.

Suppose I want to clear a Customer’s Orders.  It logically follows that each Order’s Customer property would be null at that point.  But I can’t hook in easily to the List methods to handle these operations.  **Instead of exposing the collection directly, I will expose only those operations which I support through my domain.**

Moving towards intention-revealing interfaces

Let’s fix the Customer object first.  It exposes a List directly, and allows wholesale replacement of that collection.  This is the complete antithesis of intention-revealing interfaces.  I will now only expose the sequence of Orders on Customer:

public class Customer
{
    private readonly IList<Order> _orders = new List<Order>();

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Province { get; set; }
    public IEnumerable<Order> Orders { get { return _orders; } }

    public string GetFullName()
    {
        return LastName + ", " + FirstName;
    }
}

This interface explicitly tells users of Customer two things:

  • Orders are readonly, and cannot be modified through this aggregate
  • Adding orders are done somewhere else

I now have the issue of the Order constructor needing to add itself to the Customer’s Order collection.  I want to do this:

public class Order
{
    public Order(Customer customer)
    {
        Customer = customer;
        customer.AddOrder(this);
    }

Instead of exposing the Orders collection directly, I work through a specific method to add an order.  But, I don’t want that AddOrder available everywhere, I want to only support the enforcement of the Order-Customer relationship through this explicitly defined interface.  I’ll do this by exposing an AddOrder method, but exposing it as internal:

public class Customer
{
    private readonly IList<Order> _orders = new List<Order>();

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Province { get; set; }
    public IEnumerable<Order> Orders { get { return _orders; } }

    internal void AddOrder(Order order)
    {
        _orders.Add(order);
    }

There are many different ways I could enforce this relationship, from exposing an AddOrder method publicly on Customer or through the approach above.  But either way, I’m moving towards an intention-revealing interface, and only exposing the operations I intend to support through my application.  Additionally, I’m ensuring that all invariants of my aggregates are satisfied at the completion of the Create Order operation.  When I create an Order, the domain model takes care of the relationship between Customer and Order without any additional manipulation.

If I publicly expose a collection class, I’m opening the doors for confusion and future bugs as I’ve now allowed my system to tinker with the implementation details of the relationship.  It’s my belief that the API of my domain model should explicitly support the operations needed to fulfill the needs of the application and interaction of the UI, but nothing more.

Strengthening your domain: Aggregate Construction