On the forums I see one topic coming up quite often: "Can I advise XY?". In this multipart post series I will describe the motivation for AOP and the way Spring.NET AOP (and most others like Castle.DynamicProxy and LinFu) technically work in .NET to give you a better understanding and provide you the knowledge to help answering such questions yourself.
The example: Retrying operations
Instead of the usual logging example I'd like to show you another useful feature: Retrying operations. For the sake of simplicity let's assume we're calling a webservice method for calculating the sum of two integers.
CalculatorWebService calc = new CalculatorWebService("http:/..."); int sum = calc.Add(2, 5);
Since webservices usually involve calling over the network, they are inherently unsafe and might fail for various reasons. In our application, we would like to retry webservice operations 3 times before giving up, with a 1 second delay between retries.
There are a couple of ways to implement this requirement, the most direct approach probably by deriving our own class from the webservice client class:
public class RetryingCalculatorWebService : CalculatorWebService { public RetryingCalculatorWebService(string url):base(url) {} public override int Add(int x, int y) { int retries = 0; while (true) { try { return base.Add(x, y); } catch (Exception ex) { retries++; if (retries >= 3) { throw; // retry threshold exceeded, giving up// wait a second } } } }
There are of course a couple of issues with that approach, the 2 most important are:
- The code for retrying the operation effectively buries our single line of actual business code. This makes it hard identifying what the code actually does:
public class RetryingCalculatorWebService : CalculatorWebService { public RetryingCalculatorWebService(string url):base(url) {} public override int Add(int x, int y) { int retries = 0; while(true) { try { return base.Add(x, y); } catch(Exception ex) { retries++; if (retries >= 3) { throw; // retry threshold exceeded, giving up } Thread.Sleep(1000); // wait a second } } } }
- When implementing this requirement across all our webservice clients we not only end up scattering the same lines of code all over our codebase. A change in the requirement might cause us having to change all our webservice client classes causing a huge amount of work.
A manually implemented a solution
A structured way for non-intrusively adding behavior to exisiting code is described in the GoF book, the pattern is called "Decorator", the basic idea being wrapping the actual target method with additional code. Thus you do not need to modify any existing code, instead you "chain" the various required behaviours, each behaviour implemented in its own class. In contrast to the GoF-pattern, nowadays composition is favoured over inheritance, thus instead of using the inheritance approach, let's introduce an interface to easily chain our behaviors:
public interface ICalculator { int Add(int x, int y); }
ICalculator calc = ...; int sum = calc.Add(2, 5);
Now we can easily implement our business logic and the infrastructure constraint separately and chain them later as needed:
public class CalculatorWebService : ICalculator { public CalculatorWebService(string url) { ... } public int Add(int x, int y) { // perform actual webservice call return ... } }
public class CalculatorRetryDecorator : ICalculator { private ICalculator next; public CalculatorRetryDecorator(ICalculator next) { this.next = next; } public int Add(int x, int y) { int retries = 0; while (true) { try { return next.Add(x, y); } catch (Exception ex) { retries++; if (retries >= 3) { throw; // retry threshold exceeded, giving up } Thread.Sleep(1000); // wait a second } } } }
Notice how the CalculatorRetryDecorator delegates the actual work to the next calculator in the chain. Now, whenever our requirements force us to retry calculator operations, we just "chain" the implementations together:
ICalculator calc = new CalculatorRetryDecorator( new CalculatorWebService( "http://..." ) ); int sum = calc.Add(2, 5);
Now, when we discover that our performance is to slow, well - implement a caching decorator that caches method results for a certain amount of time using the same approach and add it to the decorator chain:
ICalculator calc = new CalculatorCacheDecorator( new CalculatorRetryDecorator( new CalculatorWebService( "http://..." ) ) ); int sum = calc.Add(2, 5);
When we call the Add() method, our call graph looks like this:
This is only the beginning
We already achieved a lot by separating concerns into different classes. Still our solution has some significant weaknesses:
- When we add new methods to our ICalculator, we need to extend all of our decorators
- We want to reuse the retry logic for other services
In my next post I will address those issues and show you how we can implement a more generic solution.
Here you can download the example code for this post.
1 comment:
This is a great topic on which to start a series of posts and an excellent start with this one. Thanks for NOT using logging as your AOP example :)
And I especially love the code coloring showing how obscured the original line of code has become once wrapped in its 'retry' block -- great visual there!
Post a Comment