Code Focused
How To Refactor for Dependency Injection, Part 1: Cleaning Up
Take control of your application's behavior and move toward dependency injection by refactoring your existing code.
- By Ondrej Balas
- 05/20/2014
Dependency Injection is a programming paradigm that has recently been gaining considerable popularity within the Microsoft .NET Framework community. Applying it will force decisions to be made about how objects interact and depend on each other and will bring to light some of the problems that make code difficult to maintain. One such problem is that of circular dependencies, where two objects rely on each other. Another is complex branching: methods full of nested control statements that are almost impossible to follow. While the use of dependency injection doesn't make those problems go away, it can make them easier to spot and correct.
Even a simple application can quickly bloat into a mess as more and more requirements are added. Every change adds to the complexity of the application and every time you revisit the code, you have to follow all the branches through to understand what the code is doing. Worse yet, even code that was meant to support requirements that no longer exist must still be maintained as well. The old code never ends up being removed because, typically, no unit tests exist to make sure nothing breaks as a result of removing it. It just sticks around forever.
To illustrate how quickly such a thing can happen, I've written this simple method to determine if a store is open at the specified date and time:
public bool IsStoreOpen(DateTime dateTime)
{
return (dateTime.Hour >= 9 && dateTime.Hour < 17);
}
If the time passed in is between 9 a.m. and 5 p.m. it returns true; otherwise, it returns false. The code is easy to follow, and it's obvious what will happen just by looking at it. But then an additional requirement is added: The store is open until 9 p.m. on Friday and Saturday nights. The code to implement that is shown here:
public bool IsStoreOpen(DateTime dateTime)
{
if (dateTime.DayOfWeek == DayOfWeek.Friday || dateTime.DayOfWeek == DayOfWeek.Saturday)
{
return (dateTime.Hour >= 9 && dateTime.Hour < 21);
}
return (dateTime.Hour >= 9 && dateTime.Hour < 17);
}
The code is still easy to follow, but the complexity is growing. As time goes on, however, more and more requirements are piled onto this method. The store is only open from noon to 6 p.m. on Sunday, and opens late on Wednesday. It's also closed on New Year's Day. Different stores from within the same chain now have different hours, which must be handled as well. Stores in California are on one schedule, except store 123. In the interest of saving space, I won't show the code as it grows step-by-step in response to these changes, but know that each change typically only adds a single branch, and it's reasonable for you to add that branch. Changes like these illustrate what happens in real projects over many years, and why it's so hard to stop and say, "OK, this is the point at which I need to refactor the code." But if you never stop to refactor it before it gets too difficult, it will continue to grow until even a minor change takes hours or days of understanding before a single line of code can be written. And at that point, refactoring will seem impossible. Take a look at what the method could grow into in Listing 1.
Listing 1: The Entire Store Class After It Has Grown Considerably
public class Store
{
public string StoreNumber { get; set; }
public string State { get; set; }
public bool IsStoreOpen(DateTime dateTime)
{
if (dateTime.DayOfYear == 1 && State != "Florida")
{
return false;
}
if (State == "Florida" || (State == "California" && StoreNumber != "123"))
{
if (dateTime.DayOfWeek == DayOfWeek.Sunday)
{
return false;
}
}
if (State == "California")
{
if (StoreNumber == "123")
{
return (dateTime.Hour >= 11 && dateTime.Hour < 20);
}
if (dateTime.Hour >= 10)
{
if (dateTime.DayOfWeek == DayOfWeek.Saturday)
{
return dateTime.Hour < 20;
}
return dateTime.Hour < 18;
}
return false;
}
if (StoreNumber == "97")
{
if (dateTime.DayOfWeek == DayOfWeek.Saturday)
{
return (dateTime.Hour >= 12 && dateTime.Hour < 22);
}
if (dateTime.Hour >= 10)
{
if (dateTime.DayOfWeek == DayOfWeek.Sunday)
{
return dateTime.Hour < 17;
}
if (dateTime.DayOfWeek == DayOfWeek.Friday)
{
return dateTime.Hour < 22;
}
return dateTime.Hour < 19;
}
return false;
}
if (dateTime.DayOfWeek == DayOfWeek.Sunday)
{
return (dateTime.Hour >= 12 && dateTime.Hour < 18);
}
if (dateTime.DayOfWeek == DayOfWeek.Friday || dateTime.DayOfWeek == DayOfWeek.Saturday)
{
return (dateTime.Hour >= 9 && dateTime.Hour < 21);
}
if (dateTime.Hour < 17)
{
if (dateTime.DayOfWeek == DayOfWeek.Wednesday)
{
return dateTime.Hour >= 11;
}
return (dateTime.Hour >= 9);
}
return false;
}
}
And after taking a look at that, imagine a real project where the code within each of those branches is actually doing something other than returning a true or false.
Refactoring the Mess
As a consultant, I frequently see code just like my example, and am often brought in to help make sense of it and clean it up. These are the steps I take:
- Understand the requirements the code is meant to fulfill.
- Write some tests (either automated or a list of steps to follow manually).
- Refactor the code.
- Verify the tests still pass and requirements are being met by the new code.
I'll skip straight to the refactoring.
In the example code from Listing 1, the Store class has just the one method: IsStoreOpen. It also has two properties representing data (StoreNumber and State), but they exist only to change the behavior of the Store class. This means the IsStoreOpen method is the only thing the callers care about, and gives me an excellent starting point for extracting an interface, like this:
public interface IStore
{
bool IsStoreOpen(DateTime dateTime);
}
And now, with the interface in place, anything calling this code can stop depending on the Store class and begin depending on the IStore interface. This is beneficial because right now all of the behavior is in one place, and the interface will allow me to break it out into multiple classes. Glancing at the code I see the behavior for the state of California is relatively simple to break out, so I'll start there, leading me to the creation of the CaliforniaStore class shown in Listing 2.
Listing 2: The CaliforniaStore Class
public class CaliforniaStore : IStore
{
public string StoreNumber { get; set; }
public bool IsStoreOpen(DateTime dateTime)
{
if (StoreNumber == "123")
{
return (dateTime.Hour >= 11 && dateTime.Hour < 20);
}
if (dateTime.DayOfWeek == DayOfWeek.Sunday)
{
return false;
}
if (dateTime.Hour >= 10)
{
if (dateTime.DayOfWeek == DayOfWeek.Saturday)
{
return dateTime.Hour < 20;
}
return dateTime.Hour < 18;
}
return false;
}
}
Note that this class implements the IStore interface and still has a StoreNumber property, but is meant to encapsulate only the logic for stores in California. Branching still exists but is simple enough that the code is easy to follow. The same can be done to the Florida stores and store 97. If you'd like to see that code, it's available in the accompanying code download.
If, during refactoring, I find out the StoreNumber and State properties are actually needed somewhere other than for behavior, I can include them in the interface as well:
public interface IStore
{
string State { get; }
string StoreNumber { get; }
bool IsStoreOpen(DateTime dateTime);
}
Dependency Injection
The purpose of refactoring is two-fold. First, it makes the code more maintainable by breaking out uncontrollably branching code into separate files. As an added bonus, it relieves you of needing to maintain old code. For example, if store 97 closed, the file could be removed and no longer looked at whenever a new requirement needed to be added. Second, by replacing the original class with an interface, attention will be brought to all the areas in which that class is being instantiated. In many codebases, a common anti-pattern is for objects to instantiate (or "new up"), their dependencies within their own constructors. With that dependency changed to being an interface, it will force you to decide where the implementation will be created. This is what a caller of Store might have previously looked like:
public class StoreReporter
{
private Store store;
public StoreReporter()
{
store = new Store();
}
}
With only an interface, a decision must be made on how and where an instance of a Store object will be created. A refactoring toward dependency injection will generally leave the calling class looking like this, instead:
public class StoreReporter
{
private IStore store;
public StoreReporter(IStore store)
{
this.store = store;
}
}
In this second example, the StoreReporter object isn't creating a specific instance of a Store anymore, instead, it's requiring an implementation of one to be passed to it through its constructor. The creation of that object can be passed along up the stack, which will eventually lead you to creating all of the objects your application will need in a central location. This location is generally referred to as the Composition Root, as it is both at the root of your application and also the place in which your application is composed.
Other Benefits
Moving the responsibility of object creation to one spot within your application has a number of benefits. For one, changing behavior becomes easier because you don't need to spend time searching for where each object is created. Another benefit is something called lifecycle management. In my example, the Store object was created when the StoreReporter was created and, likewise, was destroyed with it as well. This makes sharing a single instance of a Store object across many objects difficult.
Over the next couple months, I'll continue this series on dependency injection with topics such as dependency injection containers and how they work internally, lifecycle management, how to use dependency injection in various project types and much more. Stay tuned!
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.