Interception vis-à-vis Pure DI by Mark Seemann
How do you do AOP with Pure DI?
One of my readers, Nick Ball, asks me this question:
"Just spent the last couple of hours reading chapter 9 of your book about Interceptors. The final few pages show how to use Castle Windsor to make the code DRYer. That's cool, but I'm quite a fan of Pure DI as I tend to think it keeps things simpler. Plus I'm working in a legacy C++ application which limits the tooling available to me.
"So, I was wondering if you had any suggestions on how to DRY up an interceptor in Pure DI? I know in your book you state that this is where DI containers come into their own, but I also know through reading your blog that you prefer going the Pure DI route too. Hence I wondered whether you'd had any further insight since the book publication?"
It's been more than 15 years since I last did C++, so I'm going to give an answer based on C#, and hope it translates.
Position #
I do, indeed, prefer Pure DI, but there may be cases where a DI Container is warranted. Interception, or Aspect-Oriented Programming (AOP), is one such case, but obviously that doesn't help if you can't use a DI Container.
Another option for AOP is some sort of post-processor of your code. As I briefly cover in chapter 9 of my book, in .NET this is typically done by a custom tool using 'IL-weaving'. As I also outline in the book, I'm not a big fan of this approach, but perhaps that could be an option in C++ as well. In any case, I'll proceed under the assumption that you want a strictly code-based solution, involving no custom tools or build steps.
All that said, I doubt that this is as much of a problem than one would think. AOP is typically used for cross-cutting concerns such as logging, caching, instrumentation, authorization, metering, or auditing. As an alternative, you can also use Decorators for such cross-cutting concerns. This seems daunting if you truly need to decorate hundreds, or even thousands, of classes. In such a case, convention-based interception seems like a DRYer option.
You'd think.
In my experience, however, this is rarely the case. Typically, even when applying caching, logging, or authorisation logic, I've only had to create a handful of Decorators. Perhaps it's because I tend to keep my code bases to a manageable size.
If you only need a dozen Decorators, I don't think that the loss of compile-time safety and the added dependency warrants the use of a DI Container. That doesn't mean, however, that I can't aim for as DRY code as possible.
Instrument #
If you don't have a DI Container or an AOP tool, I believe that a Decorator is the best way to address cross-cutting concerns, and I don't think there's any way around adding those Decorator classes. The aim, then, becomes to minimise the effort involved in creating and maintaining such classes.
As an example, I'll revisit an old blog post. In that post, the task was to instrument an OrderProcessor
class. The solution shown in that article was to use Castle Windsor to define an IInterceptor
.
To recapitulate, the code for the Interceptor looks like this:
public class InstrumentingInterceptor : IInterceptor { private readonly IRegistrar registrar; public InstrumentingInterceptor(IRegistrar registrar) { if (registrar == null) throw new ArgumentNullException(nameof(registrar)); this.registrar = registrar; } public void Intercept(IInvocation invocation) { var correlationId = Guid.NewGuid(); this.registrar.Register(correlationId, string.Format("{0} begins ({1})", invocation.Method.Name, invocation.TargetType.Name)); invocation.Proceed(); this.registrar.Register(correlationId, string.Format("{0} ends ({1})", invocation.Method.Name, invocation.TargetType.Name)); } }
While, in the new scenario, you can't use Castle Windsor, you can still take the code and make a similar class out of it. Call it Instrument
, because classes should have noun names, and instrument is a noun (right?).
public class Instrument { private readonly IRegistrar registrar; public Instrument(IRegistrar registrar) { if (registrar == null) throw new ArgumentNullException(nameof(registrar)); this.registrar = registrar; } public T Intercept<T>( string methodName, string typeName, Func<T> proceed) { var correlationId = Guid.NewGuid(); this.registrar.Register( correlationId, string.Format("{0} begins ({1})", methodName, typeName)); var result = proceed(); this.registrar.Register( correlationId, string.Format("{0} ends ({1})", methodName, typeName)); return result; } public void Intercept( string methodName, string typeName, Action proceed) { var correlationId = Guid.NewGuid(); this.registrar.Register( correlationId, string.Format("{0} begins ({1})", methodName, typeName)); proceed(); this.registrar.Register( correlationId, string.Format("{0} ends ({1})", methodName, typeName)); } }
Instead of a single Intercept
method, the Instrument
class exposes two Intercept
overloads; one for methods without a return value, and one for methods that return a value. Instead of an IInvocation
argument, the overload for methods without a return value takes an Action delegate, whereas the other overload takes a Func<T>.
Both overload also take methodName
and typeName
arguments.
Most of the code in the two methods is similar. While you could refactor to a Template Method, I invoke the Rule of three and let the duplication stay for now.
Decorators #
The Instrument
class isn't going to magically create Decorators for you, but it reduces the effort of creating one:
public class InstrumentedOrderProcessor2 : IOrderProcessor { private readonly IOrderProcessor orderProcessor; private readonly Instrument instrument; public InstrumentedOrderProcessor2( IOrderProcessor orderProcessor, Instrument instrument) { if (orderProcessor == null) throw new ArgumentNullException(nameof(orderProcessor)); if (instrument == null) throw new ArgumentNullException(nameof(instrument)); this.orderProcessor = orderProcessor; this.instrument = instrument; } public SuccessResult Process(Order order) { return this.instrument.Intercept( nameof(Process), this.orderProcessor.GetType().Name, () => this.orderProcessor.Process(order)); } }
I called this class InstrumentedOrderProcessor2
with the 2
postfix because the previous article already contains a InstrumentedOrderProcessor
class, and I wanted to make it clear that this is a new class.
Notice that InstrumentedOrderProcessor2
is a Decorator of IOrderProcessor
. It both implements the interface, and takes one as a dependency. It also takes an Instrument
object as a Concrete Dependency. This is mostly to enable reuse of a single Instrument
object; no polymorphism is implied.
The decorated Process
method simply delegates to the instrument
's Intercept
method, passing as parameters the name of the method, the name of the decorated class, and a lambda expression that closes over the outer order
method argument.
For simplicity's sake, the Process
method invokes this.orderProcessor.GetType().Name
every time it's called, which may not be efficient. Since the orderProcessor
class field is readonly
, though, you could optimise this by getting the name once and for all in the constructor, and assign the string to a third class field. I didn't want to complicate the example with irrelevant code, though.
Here's another Decorator:
public class InstrumentedOrderShipper : IOrderShipper { private readonly IOrderShipper orderShipper; private readonly Instrument instrument; public InstrumentedOrderShipper( IOrderShipper orderShipper, Instrument instrument) { if (orderShipper == null) throw new ArgumentNullException(nameof(orderShipper)); if (instrument == null) throw new ArgumentNullException(nameof(instrument)); this.orderShipper = orderShipper; this.instrument = instrument; } public void Ship(Order order) { this.instrument.Intercept( nameof(Ship), this.orderShipper.GetType().Name, () => this.orderShipper.Ship(order)); } }
As you can tell, it's similar to InstrumentedOrderProcessor2
, but instead of IOrderProcessor
it decorates IOrderShipper
. The most significant difference is that the Ship
method doesn't return any value, so you have to use the Action
-based overload of Intercept
.
For completeness sake, here's a third interesting example:
public class InstrumentedUserContext : IUserContext { private readonly IUserContext userContext; private readonly Instrument instrument; public InstrumentedUserContext( IUserContext userContext, Instrument instrument) { if (userContext == null) throw new ArgumentNullException(nameof(userContext)); if (instrument == null) throw new ArgumentNullException(nameof(instrument)); this.userContext = userContext; this.instrument = instrument; } public User GetCurrentUser() { return this.instrument.Intercept( nameof(GetCurrentUser), this.userContext.GetType().Name, this.userContext.GetCurrentUser); } public Currency GetSelectedCurrency(User currentUser) { return this.instrument.Intercept( nameof(GetSelectedCurrency), this.userContext.GetType().Name, () => this.userContext.GetSelectedCurrency(currentUser)); } }
This example demonstrates that you can also decorate an interface that defines more than a single method. The IUserContext
interface defines both GetCurrentUser
and GetSelectedCurrency
. The GetCurrentUser
method takes no arguments, so instead of a lambda expression, you can pass the delegate using method group syntax.
Composition #
You can add such instrumenting Decorators for all appropriate interfaces. It's trivial (and automatable) work, but it's easy to do. While it seems repetitive, I can't come up with a more DRY way to do it without resorting to some sort of run-time Interception or AOP tool.
There's some repetitive code, but I don't think that the maintenance overhead is particularly great. The Decorators do minimal work, so it's unlikely that there are many defects in that area of your code base. If you need to change the instrumentation implementation in itself, the Instrument
class has that (single) responsibility.
Assuming that you've added all desired Decorators, you can use Pure DI to compose an object graph:
var instrument = new Instrument(registrar); var sut = new InstrumentedOrderProcessor2( new OrderProcessor( new InstrumentedOrderValidator( new TrueOrderValidator(), instrument), new InstrumentedOrderShipper( new OrderShipper(), instrument), new InstrumentedOrderCollector( new OrderCollector( new InstrumentedAccountsReceivable( new AccountsReceivable(), instrument), new InstrumentedRateExchange( new RateExchange(), instrument), new InstrumentedUserContext( new UserContext(), instrument)), instrument)), instrument);
This code fragment is from a unit test, which explains why the object is called sut
. In case you're wondering, this is also the reason for the existence of the curiously named class TrueOrderValidator
. This is a test-specific Stub of IOrderValidator
that always returns true
.
As you can see, each leaf implementation of an interface is contained within an InstrumentedXyz
Decorator, which also takes a shared instrument
object.
When I call the sut
's Process
method with a proper Order
object, I get output like this:
4ad34380-6826-440c-8d81-64bbd1f36d39 2017-08-25T17:49:18.43 Process begins (OrderProcessor) c85886a7-1ce8-4096-8a30-5f87bf0014e3 2017-08-25T17:49:18.52 Validate begins (TrueOrderValidator) c85886a7-1ce8-4096-8a30-5f87bf0014e3 2017-08-25T17:49:18.52 Validate ends (TrueOrderValidator) 8f7606b6-f3f7-4231-808d-d5e37f1f2201 2017-08-25T17:49:18.53 Collect begins (OrderCollector) 28250a92-6024-439e-b010-f66c63903673 2017-08-25T17:49:18.55 GetCurrentUser begins (UserContext) 28250a92-6024-439e-b010-f66c63903673 2017-08-25T17:49:18.56 GetCurrentUser ends (UserContext) 294ce552-201f-41d2-b7fc-291e2d3720d6 2017-08-25T17:49:18.56 GetCurrentUser begins (UserContext) 294ce552-201f-41d2-b7fc-291e2d3720d6 2017-08-25T17:49:18.56 GetCurrentUser ends (UserContext) 96ee96f0-4b95-4b17-9993-33fa87972013 2017-08-25T17:49:18.57 GetSelectedCurrency begins (UserContext) 96ee96f0-4b95-4b17-9993-33fa87972013 2017-08-25T17:49:18.58 GetSelectedCurrency ends (UserContext) 3af884e5-8e97-44ea-aa0d-2c9e0418110b 2017-08-25T17:49:18.59 Convert begins (RateExchange) 3af884e5-8e97-44ea-aa0d-2c9e0418110b 2017-08-25T17:49:18.59 Convert ends (RateExchange) b8bd0701-515b-44fe-949f-5f5fb5a4590d 2017-08-25T17:49:18.60 Collect begins (AccountsReceivable) b8bd0701-515b-44fe-949f-5f5fb5a4590d 2017-08-25T17:49:18.60 Collect ends (AccountsReceivable) 8f7606b6-f3f7-4231-808d-d5e37f1f2201 2017-08-25T17:49:18.60 Collect ends (OrderCollector) beadabc4-df17-468f-8553-34ae4e3bdbfc 2017-08-25T17:49:18.60 Ship begins (OrderShipper) beadabc4-df17-468f-8553-34ae4e3bdbfc 2017-08-25T17:49:18.61 Ship ends (OrderShipper) 4ad34380-6826-440c-8d81-64bbd1f36d39 2017-08-25T17:49:18.61 Process ends (OrderProcessor)
This is similar to the output from the previous article.
Summary #
When writing object-oriented code, I still prefer Pure DI over using a DI Container, but if I absolutely needed to decorate many services, I'd seriously consider using a DI Container with run-time Interception capabilities. The need rarely materialises, though.
As an intermediate solution, you can use a delegation-based design like the one shown here. As always, it's all a matter of balancing the constraints and goals of the specific situation.