When people speak of the object-relational impedance mismatch, they usually talk about the differences between structure, the lack of behavior in relational databases, and the complexities that mapping abstractions present. A disadvantage which receives less attention is the impact that the impedance mismatch has on keeping code free of extraneous data-driven communications. In spite of the efforts taken to avoid it, domain layer code ultimately includes some degree of coding and/or structure which was dictated by the constraints of the data layer.
To illustrate, assume a parent object contains a collection of children objects. To avoid having the entire object graph loaded from the database all at once, the children objects may be loaded lazily so that they are not pulled from the database until necessary. One pedagogical approach is to initialize a private collection to null and populate it with a collection of objects when its publicly exposed getter is accessed:
public class Parent {
public Children Children {
get {
if (children == null) {
// Init and populate 'children' member
}
return children;
}
}
private children = null;
}
This is a common approach (albeit, not a particularly good one) when using a custom developed data mapper without the use of a service layer. The problem with this is that seldom does a domain rule exist dictating that a child collection be loaded lazily. Obviously, loading collections lazily is imperative to the performance of an application and is folly to avoid. But herein lies the problem; modifications are being made to accommodate a data-layer concern within the domain. The problem does not easily dissipate with the introduction of object-relational mappers such as NHibernate. With NHibernate 1.0 and .NET 2.0, it was near impossible to not only code for the data layer, but to include references to NHibernate services directly. Ayende's, now deprecated, NHibernate Generics was a perfect example of this. If you wanted to leverage the benefits of generics within .NET 2.0, you had to clutter the domain layer with code which only existed for the support of the data layer. (But thank goodness he wrote it for us at the time!)
With NHibernate 1.2 and the introduction of native support for generics, Ayende's code was no longer necessary for "out of the box" lists, but a simple solution for leveraging custom collections was still not obvious (to me anyway). By "custom collection," I mean a collection which extends the behavior of an existing .NET collection(s) with modified or new functionality. An example would be adding a FindAllByDateRange method onto an Orders collection that is associated with a Customer object. For this instance in particular (no mean for the pun), you can accommodate the FindAllByDateRange method via four primary routes:
-
The FindAllByDateRange method could be added to a
repository object and the database could do the filtering itself. This is very useful, if not mandatory, for very large collections. But this also has a few downsides. A (mostly
aesthetic) downside is that moving the filtering to the data-layer grays the layer between the
responsibilities of the domain and that of the data layer. It's a regular occurrence for a developer to ponder if a particular bit of behavior belongs with the domain or with the data layer. Having many such methods within the data-layer makes it difficult to reuse such functionality in a variety of ways, such as with the
specification pattern in the domain layer, and it makes it more difficult for future maintainers to decide where they should put their code. This is similar to the dilemmas we ran into before ORMs when having to decide how much thinking stored procs should be doing. My personal rule of thumb is that unless there is an obvious performance reason for putting behavior within the data-layer, then you should lean towards putting it into the domain. (Yes, I realize that the ICriteria object essentially
is the
specification pattern; but it is a data-centric tool and feels awkward when used in an otherwise pure domain layer.)
-
The FindAllByDateRange method could be placed onto the Customer object. In this way, you wouldn't have to do anything special with the Orders collection other than declaring them as an IList<Orders> collection and bind to it in the NHibernate configuration, accordingly. This approach quickly degrades the Customer object as the
single responsibility principle becomes violated over and over again as more and more collection-oriented responsibilities are given to the parent object. Additionally, this approach now forces the Customer object to have a relationship to every collection that it could conceivably traverse in one association, since it is now responsible for the domain level filtering of those collections.
-
The FindAllByDateRange method could be placed into a wrapper collection which holds a pointer to the collection managed by NHibernate. This was the approach I espoused in my article,
NHibernate (Not So Bad) Practices with ASP.NET. The following is a code example which demonstrates the technique of wrapping the collection to extend it with additional functionality:
// Within Customer.cs...
public Orders Orders {
get {
if (ordersWrapper == null) {
ordersWrapper = new Orders(orders);
}
return ordersWrapper;
}
}
private Orders ordersWrapper;
// NHibernate binds directly to this member via the access="field" setting
private IList orders = new List<Order>();
////////////////////////
// Within Orders.cs
public Orders(IList orders) {
this.orders = orders;
}
public void FindAllByDateRange(DateRange dateRange) {
// filter the list by date range
return ordersFilteredByDateRange;
}
private IList orders;
The problem with this approach is that it again necessitates that we modify the domain layer to fit the needs of the data-layer. It doesn't necessarily introduce anything data-layer specific, but it introduces enough data-driven complexity to lead to increased difficulty in maintainability with higher likelihood for the introduction of bugs.
-
Finally, the FindAllByDateRange could be added to an NHibernate.UserTypes.IUserCollectionType custom collection and treated like a first class citizen, no longer the red-headed step-child that we've been tormenting it as. (No offense to any red-headed step-children out there!) The major drawback that people run into with this approach is that the domain-layer usually ends up with a dependency to the NHibernate assembly or to other data-related assemblies to provide the support for the NHibernate custom collection. But this need not be the case.
Leveraging NHibernate's support for custom collections with the IUserCollectionType interface is the best solution I have found for removing data-driven influences on domain-layer code. By further leveraging the added benefits of dependency inversion, the domain-layer need not have direct dependencies on NHibernate.dll or any other data-related assemblies. The intention is to simplify the domain-layer code to something akin to:
public class Customer {
public IOrders Orders {
get { return orders; }
protected set { orders = value; }
}
private IOrders orders = new Orders();
}
public interface IOrders : IList<Order> {
IOrders FindAllByDateRange(DateRange dateRange);
}
This post is the first of three; the next two will describe the following:
-
Part II: The Basics. This post will describe the basics for understanding how NHibernate custom collections are created and managed with a basic working example.
-
Part III: Refactored. This post will take the code presented in "The Basics" and separate the responsibilities into appropriately tiered-packages, leveraging dependency inversion, so that the domain layer need not concern itself with data-related dependencies.
-
Part IV: Extensions. This final post throws everything out the window and solves the problem using extension methods. A great approach if you're using .NET 3.0 or later.
Before proceeding with the next three parts, I invite you to review the excellent work which helped lead me to the non-extension method solution that I will be presenting: http://analog-man.blogspot.com/2007/01/bridge-gap-between-your-nhibernate.html and http://damon.agilefactor.com/2007/07/nhibernate-custom-collections.html.
Billy McCafferty
Posted
12-03-2007 11:49 PM
by
Billy McCafferty