Wednesday, March 10, 2010

Judiciously Exposing Collections in the Domain

This is a follow-up to Jimmy Bogard’s blog post found here: http://www.lostechies.com/blogs/jimmy_bogard/archive/2010/03/10/strengthening-your-domain-encapsulated-collections.aspx

Jimmy does a good job of explaining why exposing your collections as IList or IList<T> is likely not the best choice for your domain.  His basic argument is that collections should only be exposed as immutable collections to client code.

Like Jimmy, I often expose collections as IEnumerable<T> but have run into problems with that too.  While IList<T> with its 9 methods and 3 properties is  exposing more than you want to, IEnumerable<T> often proves too limited with only its single GetEnumerator() method.  I have found that creating custom collection classes has become an increasingly valuable method for showing intent.

Let’s take a very simple example, we want to expose a count of the orders that a customer has, and yes, I know there is a Count extension method on IEnumerable<T>, but bear with me, we’re starting simple here.  Using Jimmy’s Customer/Order example, our classes would look like this:

public class Customer
{
public OrderCollection Orders { get; private set; }
}

public class OrderCollection : IEnumerable<Order>
{
private IList<Order> orders;

public int Count
{
get { return orders.Count; }
}

public OrderCollection(IEnumerable<Order> orders)
{
this.orders = new List<Order>(orders);
}

public IEnumerator<Order> GetEnumerator()
{
return orders.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

Notice that you can still use the Orders property on Customer as an IEnumerable<Order> and we’ve also added a Count property.  That was a very trivial example, so let’s look at something more interesting.

One of our requirements is to track the customer’s most recent order, so let's add this method to OrderCollection:

    public Order GetMostRecent()
{
return (
from o in orders
orderby o.OrderDate descending
select o
).FirstOrDefault();
}

We've added a method to our collection class that clearly states its intention.  We haven’t over exposed other methods in order to achieve the requirement.  We can test this method in isolation from the Customer class and we’ve encapsulated the logic and abstracted it away from other code.

There’s all sorts of methods that we could add to OrderCollection as required in our domain.  We could add a method that returned all of the unshipped orders, or the delinquent orders.  Whatever our domain requires, we have a place in our domain where the code belongs.

Collections within the domain are often over exposed and not well encapsulated.  By creating simple custom collection classes we create intention revealing classes that don’t expose unwanted behaviour.  They make the domain more discoverable, testable and encapsulated.

No comments: