Let's say that your class needs to do 3 things for a certain operation:
- Perform a security check;
- Log the method call;
- Cache the result.
Let's further assume that your class doesn't know anything about the specific way you've configured your security, logging, or caching. You need to depend on abstractions of these things.
There are a few ways to go about it. One way would be to set up a bunch of interfaces and use constructor injection:
public class OrderService : IOrderService
{
private readonly IAuthorizationService auth;
private readonly ILogger logger;
private readonly ICache cache;
public OrderService(IAuthorizationService auth, ILogger logger,
ICache cache)
{
if (auth == null)
throw new ArgumentNullException("auth");
if (logger == null)
throw new ArgumentNullException("logger");
if (cache == null)
throw new ArgumentNullException("cache");
this.auth = auth;
this.logger = logger;
this.cache = cache;
}
public Order GetOrder(int orderID)
{
auth.AssertPermission("GetOrder");
logger.LogInfo("GetOrder:{0}", orderID);
string cacheKey = string.Format("GetOrder-{0}", orderID);
if (cache.Contains(cacheKey))
return (Order)cache[cacheKey];
Order order = LookupOrderInDatabase(orderID);
cache[cacheKey] = order;
return order;
}
}
This isn't horrible code, but think of the problems we're introducing:
The OrderService
class can't function without all three dependencies. If we want to make it so it can, we need to start peppering the code with null checks everywhere.
We're writing a ton of extra code to perform a relatively simple operation (looking up an order).
All this boilerplate code has to be repeated in every method, making for a very large, ugly, bug-prone implementation.
Here's a class which is much easier to maintain:
public class OrderService : IOrderService
{
[Authorize]
[Log]
[Cache("GetOrder-{0}")]
public virtual Order GetOrder(int orderID)
{
return LookupOrderInDatabase(orderID);
}
}
In Aspect Oriented Programming, these attributes are called Join Points, the complete set of which is called a Point Cut.
Instead of actually writing dependency code, over and over again, we leave "hints" that some additional operations are supposed to be performed for this method.
Of course, these attributes have to get turned into code sometime, but you can defer that all the way up to your main application code, by creating a proxy for the OrderService
(note that the GetOrder
method has been made virtual
because it needs to be overridden for the service), and intercepting the GetOrder
method.
Writing the interceptor might be as simple as this:
public class LoggingInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
if (Attribute.IsDefined(invocation.Method, typeof(LogAttribute))
{
Console.Writeline("Method called: "+ invocation.Method.Name);
}
invocation.Proceed();
}
}
And creating the proxy would be:
var generator = new ProxyGenerator();
var orderService = (IOrderService)generator.CreateClassProxy(typeof(OrderService),
new LoggingInterceptor());
This is not only a lot less repetitive code, but it completely removes the actual dependency, because look what we've done - we don't even have an authorization or caching system yet, but the system still runs. We can just insert the authorization and caching logic later by registering another interceptor and checking for AuthorizeAttribute
or CacheAttribute
.
Hopefully this explains the "why."
Sidebar: As Krzysztof Koźmic comments, it's not a DP "best practice" to use a dynamic interceptor like this. In production code, you don't want to have the interceptor running for unnecessary methods, so use an IInterceptorSelector instead.