In a follow-up to his earlier post on Constructor Over-Injection, Jeffrey Palermo changes his stance on Constructor Over-Injection from anti-pattern to the more palatable code smell. In this post I introduce the concept of an Aggregate Service and outline a refactoring that addresses this code smell.
If I should extract a core message from Jeffrey Palermo’s blog post it would be that it’s a code smell if you have a class that takes too many dependencies in its constructor.
I can only agree, but only so far as it’s a code smell. However, it has nothing to do with DI in general or Constructor Injection specifically. Rather, it’s a smell that indicates a violation of the Single Responsibility Principle (SRP). Let’s review the example constructor:
public OrderProcessor(IOrderValidator validator,
IOrderShipper shipper,
IAccountsReceivable receivable,
IRateExchange exchange,
IUserContext userContext)
In this version, I even added IOrderShipper back in as I described in my earlier post. Surely, five constructor parameters are too many.
Constructor Injection makes SRP violations glaringly obvious.
What’s not to like? My personal threshold lies somewhere around 3-4 constructor parameters, so whenever I hit three, I start to consider if I could perhaps aggregate some of the dependencies into a new type.
I call such a type an Aggregate Service. It’s closely related to Parameter Objects, but the main difference is that a Parameter Object only moves the parameters to a common root, while an Aggregate Service hides the aggregate behavior behind a new abstraction. While the Aggregate Service may start its life as a result of a pure mechanistic refactoring, it often turns out that the extracted behavior represents a Domain Concept in its own right. Congratulations: you’ve just move a little closer to adhering to the SRP!
Let’s look at Jeffrey Palermo’s OrderProcessor example. The core implementation of the class is reproduced here (recall that in my version, IOrderShipper is also an injected dependency):
public SuccessResult Process(Order order)
{
bool isValid = _validator.Validate(order);
if (isValid)
Collect(order);
_shipper.Ship(order);
}
return CreateStatus(isValid);
private void Collect(Order order)
User user = _userContext.GetCurrentUser();
Price price = order.GetPrice(_exchange, _userContext);
_receivable.Collect(user, price);
If you examine the code it should quickly become apparent that the Collect method encapsulates a cluster of dependencies: IAccountsReceivable, IRateExchange and IUserContext. In this case it’s pretty obvious because they are already encapsulated in a single private method. In real production code, you may need to perform a series of internal refactorings before a pattern starts to emerge and you can extract an interface that aggregates several dependencies.
Now that we have identified the cluster of dependencies, we can extract an interface that closely resembles the Collect method:
public interface IOrderCollector
void Collect(Order order);
In lieu of a better name, I simply chose to call it IOrderCollector, but what’s interesting about extracting Aggregate Services is that over time, they often turn out to be previously implicit Domain Concepts that we have now dragged out in the open and made explicit.
We can now inject IOrderCollector into OrderProcessor and change the implementation of the private Collect method:
_collector.Collect(order);
Next, we can remove the redundant dependencies, leaving us with this constructor:
IOrderCollector collector)
With three constructor parameters it’s much more acceptable, but we can always consider repeating the procedure and extract a new Aggregate Service that aggregates IOrderShipper and IOrderCollector.
The original behavior from the Collect method is still required, but is now implemented in the OrderCollector class:
public class OrderCollector : IOrderCollector
private readonly IUserContext _userContext;
private readonly IRateExchange _exchange;
private readonly IAccountsReceivable _receivable;
public OrderCollector(IAccountsReceivable receivable,
_receivable = receivable;
_exchange = exchange;
_userContext = userContext;
#region IOrderCollector Members
public void Collect(Order order)
Price price =
order.GetPrice(_exchange, _userContext);
#endregion
Here’s another class with three constructor parameters, which falls within the reasonable range. However, once again, we can begin to consider whether the interaction between IUserContext and the Order could be better modeled.
In outline form, the Introduce Aggregate Service refactoring follows these steps:
The beauty of Aggregate Services is that we can keep wrapping one Aggregate Service in new Aggregate Services to define more and more coarse-grained building blocks as we get closer and closer to the application boundary.
Keeping each class and its dependencies to simple interactions also makes it much easier to unit test all of them because none of them do anything particularly complex.
Adhering strictly to Constructor Injection makes it easy to see when one violates the SRP and should refactor to an Aggregate Service.
Remember Me
a@href@title, b, em, i, strike, strong
Page rendered at Thursday, March 11, 2010 1:04:39 PM (Romance Standard Time, UTC+01:00)
Twitter Updates
Disclaimer The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.