In this post, we will discuss the process we use to refactor an existing application to use Inversion of Control. The main goal of this process (aside from the obvious) is to keep the application in a release state throughout the process. As long as they are done in order, these steps can be done in any sprint and the application may be released at sprint end.
For this post to make sense, there were some assumptions we had to make while writing it.
Assumption 1: .NET
The first assumption is that the target application is written in ASP.NET. Unfortunately, there is no IoC Container that exists for Classic ASP, so converting an existing ASP application is not possible. This article DOES apply for hybrid Classic/ASP.NET projects, but only for the .NET side.
Assumption 2: Continuous Deployment
Alobria is an Agile development shop, and we practice continuous deployment and continuous integration. The process for introducing IoC embraces these practices by breaking the process up into small, stand-alone steps. Each of the steps below may be done individually and in a single sprint without making the application unreleasable. During sprint planning, you can pick up one or more of these tasks along with any other backlog items, and still release at the end of the sprint.
We developed the process like this because the majority of our work is with existing systems that cannot be taken down for maintenance. The steps were designed to never get in the way of the release schedule.
Though Inversion of Control is a software architectural pattern that has been around for a while, there is a significant number of legacy applications that do not use it, but would benefit from the loose coupling it provides. The most common implementation of IoC, Dependency Injection, is difficult to implement in existing applications. A basic assumption of IoC Containers is that all necessary objects can be resolved using the container. This means that legacy applications must be converted all-or-nothing. Each and every class and service must be converted to DI in one release for it to work.
To see why this is so, consider the following dependency graph.
In this graph we can see that the classes are tightly coupled. In other words, there are hard references from one class to another. If we were to try to convert this application to Dependency Injection, we might start with the Controller. However, that requires references to the DAL, EmailService, APIServices, and ErrorLogger so the IoC Container must be able to resolve each of these for the controller. To be resolved, each class that these classes depend upon must also be resolvable, etc. As the dominoes fall, we see that we must convert all classes at once or this approach will not work.
However, there is a solution. This article will lead step by step through the process of converting an existing application to use Dependency Injection, without the need for rewriting the application or taking it down for an extended period of time.
One of the core reasons we convert applications to IoC is for loose coupling. This is achieved by updating the calling code to use an interface instead of public methods on the object itself. (There are many discussions of when, why, and how Interfaces should be used – check here, here, and here.) This requires that all objects we want to store in the container be associated with an interface. Luckily, converting an object to an interface is a trivial prospect.
Interfaces can be introduced without changing the logic of any of your code. It is simply a slight change in references in the code. Logically, the execution is exactly the same, which minimizes the QA requirements.
First decide what namespace you will put the interface in. You can put the interface in the same project as the logic, but i prefer to have it in a different namespace (project). I normally have a project called Infrastructure where I keep generic logic that has no knowledge of the specific application, such as string manipulation utilities, an email sending service, etc. If you have a similar project, this is a good location for the interface, or you can create a project specifically for holding interfaces.
When creating the interface itself and attaching it to the class, there are many automated tools that can help. If you don’t happen to have one handy, you can easily create an interface by hand by simply exposing all the public methods and properties in the concrete class. For example, consider the following class.
Implementing an interface for this class is a matter of simply copying the signature of each public method or property. Here is the interface for this class.
Once this is done, scan through the code and ensure that any references to the concrete class are changed to be references to the interface. The only exceptions are the locations where the class is created – these must remain references to the class. For example, this is how our Example class would have been used before introducing the interface.
This is how the calling code would look after converting to the interface.
Add IoC container
Once the interfaces are added to your project, you must add the IoC container. There are many containers available for download, and all have their pros and cons. Which one you pick is up to you and will not affect how you proceed with your conversion.
The container should be initialized once at application startup. It needs to be configured to provide the proper services in response to requests made by the application. In other words, it needs to be setup so when your application asks it to resolve one of the newly created interfaces, it knows which concrete class to actually create. Many of the IoC containers available have an option that scans a given DLL or namespace and automatically maps the given interfaces. This is an excellent way to go and can save a lot of configuration time as you add classes to your container.
Once the container is installed, remember to add unit tests to ensure that each class can be resolved as necessary.
A Note for Agile Shops
Something to note is that we are only introducing the IoC container, we are not converting any code to use it yet. After this step is complete, you will have a fully configured container that is never used (except in your unit tests). So far, we have not changed any logic anywhere in the application so everything should continue to work exactly as it did before. I point this out because if you are an Agile shop with short sprints (many of our clients have 1 week sprints), this is a good place to stop for the sprint. Everything on this page is easily done within the confines of a single sprint, and you can still take on other tasks that add features or fix defects.
In the next post, we will discuss how to use this foundation to complete the installation of an IoC container and Dependency Injection.