Code Focused
How To Refactor for Dependency Injection, Part 3: Larger Applications
Ondrej Balas continues his series on refactoring code for dependency injection, focusing on techniques that make it easier to refactor complex applications.
- By Ondrej Balas
- 07/16/2014
Dependency injection is a programming paradigm that enables you to write much more maintainable code by promoting loose coupling of objects. (In case you've missed them, this article is a continuation of Part 1 and Part 2 in my series on dependency injection. ) In a new or small application, refactoring to dependency injection is relatively straight forward. As the application to be refactored grows larger, however, it can become quite the undertaking. Fortunately, there's a way to simplify the problem and refactor your application piece by piece.
Refactoring a Complex Application
Before you can begin refactoring, you need to know what to look for. I'm a very visual person, so whenever I work on refactoring a large or complex application, I begin by whiteboarding all components and their interactions. Seeing all the projects on the board allows me to easily see where the entry point into the application is, which is where I want to put my Composition Root.
I then step through the code, stepping into every constructor, and look for constructors that new up objects within themselves, like this one:
public NotificationEngine()
{
_dataStream = new SomeDataStream();
_emailSender = new EmailSender();
_recipientAddress = new ConfigurationReader().GetNotificationRecipientAddress();
_logger = new FileSystemLogger("somepath.txt");
}
When I see this, it's almost guaranteed the objects being created here have their own constructors just like this one, creating a complex chain of constructors. And if that isn't bad enough, the objects are likely also being created in other places, making things even more confusing.
As I mentioned earlier in the series, the goal is to eventually replace constructors like that with constructors that take their dependencies as parameters rather than creating them, like this:
public NotificationEngine(IDataStream dataStream, IEmailSender emailSender,
IConfigurationReader configurationReader, ILogger logger)
{
_dataStream = dataStream;
_emailSender = emailSender;
_recipientAddress = configurationReader.GetNotificationRecipientAddress();
_logger = logger;
}
In a complex application, this is much easier said than done. A common stepping stone is to create the new constructor, but leave the original default constructor in place. The original constructor should then be modified to call into the new constructor, like this:
public NotificationEngine()
: this(new SomeDataStream(),
new EmailSender(),
new ConfigurationReader(),
new FileSystemLogger("somepath.txt"))
{
}
Not breaking any existing code allows you to refactor the application one piece at a time. And when you use a Dependency Injection (DI) container to create the objects, most containers will automatically select the more complicated constructor rather than the default one (more on this later).
This technique, known as "Bastard Injection," is controversial and generally considered an anti-pattern. This is because it's often used only as a way to facilitate unit testing, while the default constructors are still the only constructors used in production code. It can also cause the code to implicitly create dependencies on assemblies that would otherwise not need to be referenced. For example, if IEmailSender was an interface defined in the same project as the constructor, but EmailSender wasn't, creating the EmailSender within the constructor would create a dependency on that other assembly.
Still, I find Bastard Injection an incredibly useful stopgap in practice -- but it shouldn't be the end goal.
Once I've completed a first pass through the application, I start looking for the "new" keyword used in other places. Another common thing I see is fields that have the new implementations given to them and no constructor present, like this:
public class NotificationEngine
{
private readonly IDataStream _dataStream = new SomeDataStream();
private readonly IEmailSender _emailSender = new EmailSender();
private readonly string _recipientAddress = new ConfigurationReader().GetNotificationRecipientAddress();
private readonly ILogger _logger = new FileSystemLogger("somepath.txt");
// The rest of the class
}
This should be refactored just as if all object instantiation was taking place inside the default constructor.
Setter Injection
So far, I've been using Constructor Injection, which is by far the most commonly used form of DI. Depending on the scenario, other patterns might be more effective. One such pattern, Setter Injection (also known as Property Injection), is helpful when there's a sensible and local default for the dependency being set. Take the code in Listing 1, for example.
Listing 1: An implementation of Setter Injection using Ninject attributes
private IEmailSender emailSender;
[Inject, Optional]
public IEmailSender EmailSender
{
get
{
if (emailSender == null)
{
emailSender = new DefaultEmailSender();
}
return emailSender;
}
set { emailSender = value; }
}
Rather than setting the EmailSender property within the constructor, it's tagged with the Inject and Optional attributes. The Inject attribute tells Ninject to set that property immediately after the object is created, but that, alone, might not be enough. If the Optional attribute isn't also set, Ninject will throw an exception if there's no binding present for IEmailSender. By combining both attributes, you can create a property that's set only if a binding exists -- otherwise, it defaults to the DefaultEmailSender.
Handling Multiple Constructors
When dealing with multiple constructors, it's important to know which one will be called by the DI container when an object's requested. This varies slightly from container to container, but the general principle is that the container will use the constructor with the most parameters it can resolve completely. For example, assume you have three constructors:
1: public Quirble()
2: public Quirble(IFoo foo)
3: public Quirble(IFoo foo, IBar bar)
If the container only has a binding for IFoo, but not IBar, it will select constructor No. 2. If it has a binding for both IFoo and IBar, it will select constructor No. 3. If it has neither binding, it will select the default constructor (No. 1). With Ninject, you can force a specific constructor to be called using the Inject attribute:
1: [Inject] public Quirble()
By using the Inject attribute, you're telling Ninject to always use that constructor and ignore all other constructors.
There are some special cases in which some containers don't behave the same as others. Take this example:
1: public Quirble(IFoo foo)
2: public Quirble(IBar bar)
In this case, if the container has a binding for both IFoo and IBar, Ninject will throw an exception due to an ambiguous constructor. In the same situation, Castle Windsor will select one of the constructors. In fact, according to its documentation, "If there are more than one greediest constructor, it will use any of them. It is undefined which one and you should not depend on Windsor always picking the same."
Break it Down
Refactoring a complex application to use dependency injection can seem intimidating, but with techniques such as Bastard Injection it can be broken down into many smaller refactorings. You can also use techniques like setter injection in places where switching to constructor injection will be difficult. And be forewarned that different containers behave in different ways, so whenever you'e not sure how your container will behave, it's always a good idea to check out the documentation.
Up next: XML configuration, a look at various DI containers and how to create a plug-in architecture with the use of convention-based binding.
About the Author
Ondrej Balas owns UseTech Design, a Michigan development company focused on .NET and Microsoft technologies. Ondrej is a Microsoft MVP in Visual Studio and Development Technologies and an active contributor to the Michigan software development community. He works across many industries -- finance, healthcare, manufacturing, and logistics -- and has expertise with large data sets, algorithm design, distributed architecture, and software development practices.