Eamonn McEvoy

@eamonnmcevoy

A couple of decoupling techniques in .net

To me, legacy code is simply code without tests.
— Michael C. Feathers, author ‘Working Effectively with Legacy Code’

Most of us will work with this type of legacy code at some stage. Trying to add tests can be very difficult. If the code has been written without tests, then chances are it is tightly coupled with other parts of the system. In order to work effectively and produce robust solutions we need to have a few techniques in our arsenal to make the code testable. After reading ‘The Art of Unit Testing’ by Roy Osherove I’ve been experimenting with the techniques recommended in the book. Below, I’ll work through the 3 techniques I use most regularly.

Example: Decoupling ReportGenerator

Here we have an example of the kind of tight coupling you might encounter in a legacy application. The ReportGenerator is used to fetch a block of text containing a user’s name, their results, and the date the report was generated.

The output will look something like this:

Joe Bloggs
A,B,A,C
Date: 29/03/2017

The class contains 3 tight couplings affecting our ability to write tests. UserService and ResultService both make requests to our database which we must be able to mock. The call to DateTime.Now also affects our ability to write tests as it will return a different string every time it is called.

Technique 1. Dependency Injection

Instead of constructing the Services within our GetUserReport method we can pass in interfaces via the constructor. First we need to extract interfaces for our Services (with a bit of resharper magic…):

internal interface IUserService {    
User GetUser(int userId);
}
internal interface IResultService {    
string[] GetUserResults(int userId);
}

Then modify the ResultGenerator to have its dependencies injected:

Now we are able to mock our Services and test the ResultGenerator. This is a common way to achieve of decoupling and is often recommended in clean coding guides and discussions on the SOLID principles.

Whilst this solves our coupling problem, it sometimes results in an enormous amount of work. What if our result generator is called from many different locations? Then we must create instances of our services in every file where it is used; suddenly we have gone from writing a simple test to modifying a large number of files. Also, if there are many dependencies in the class, the constructor can become unwieldy due to an ever-growing list of parameters. This technique hasn’t helped us break the dependency on our system call to DateTime.Now.

Technique 2. Overridable Properties

To minimise the impact of introducing tests to the ResultGenerator we may wish to avoid modifying the constructor. We can still achieve the ability to provide mock implementations by wrapping our service calls in CLR properties and lazy loading the implementation.

With this technique we can leave most of our code as it is and simply override the service implementations in our test code.

This solution is preferable where we want to confine the scope of our changes. However it still doesn’t help us with the DateTime problem.

Technique 3. Protected Virtual Methods

This is a clever approach recommended in The Art of Unit Testing. It is similar to Technique 2, but also addresses our need to mock the system call to DateTime. If we wrap our dependencies in protected virtual methods we can provide mocks by creating a derived TestableResultGenerator which overrides these virtual methods:

Then we create a TestableResultGenerator in our test code which derives from ResultGenerator and overrides the available virtual methods:

This testable implementation allows us to provide mock implementations in our tests.

This is somewhat controversial since we aren’t directly accessing the code under test, but through a proxy.

Conclusion

Decoupling legacy code can be a difficult problem. It requires a significant amount of upfront work for little immediate benefit. This can be an especially hard sell to management when the pressure is on to meet deadlines; however the increased quality and maintainability of the code base has very tangible benefits in the long run.

Mastering these techniques will ease the process of decoupling. Using combinations of these techniques will provide elegant solutions to most problems you will encounter.

More by Eamonn McEvoy

Topics of interest

More Related Stories