If you’ve never done Test Driven Development or aren’t even sure what this "crazy TDD stuff” is all about than this is the series for you. Over the next 30 days this series of posts take you from “I can spell TDD” to being able to consider yourself a “functional” TDD developer. Of course TDD is a very deep topic and truly mastering it will take quite a bit of time, but the rewards are well worth it. Along the way I’ll be showing you how tools like JustCode and JustMock can help you in your practice of TDD.
In this series we’ve already discussed the basics of TDD by creating our first test and then making that test pass. Last time I discussed the SOLID principles and how they help us in our practice of TDD. This time we’ll put two of the SOLID principles into practical application by discussing Dependency Injection.
Previous Posts in this Series Day Five – Make Your Code SOLID
As I’ve previously said in this series, coupling and binding in software is a fact of life. As developers we try to make our applications as loosely coupled as possible, but at the end of the day we have to bind to something in order for the various components we create to be usable. A few commonly used analogies for a loosely coupled applications are Tinker Toys and Lego's. The idea being that if you build your application as a series of components which conform to a standardized interface then you should be able to compose your application from these various components, swapping them in and out as needed.
That sounds great, but how do we apply this concept to code? To answer that question we turn to “Uncle” Bob Martin’s SOLID principles which were covered in the previous post. While all five principles contribute to the ability to create a loosely coupled application, two specifically spell out how to accomplish this.
The Liskov Substitution Principle (LSP) states that we can substitute a derived class for a base class without breaking the application. We can extend that concept to interfaces by saying classes that implement a common interface (a common public API) may be substituted for one another; a bird, a plane and Superman are very different from each other, but all can fly. So long as they implement the IFly interface, and I only care about having a component that does, I can use any of them and I don’t necessarily case which I use.
The Dependency Inversion Principle (DIP) tells us that our code should depend on abstractions, not concrete implementations. My application may need a data store. If I’m bound to a concrete instance of a relational database, like SQL Server then I have closed myself off to other implementations of data stores, like file systems, web services, object databases or anything else that I could use to hold data. This design hard-wires a limitation into my application by being reliant on a concrete type of data store instead of the concept of “something that can store data.”
By leveraging the concepts of LSP and DIP we can derive a methodology to create applications with loosely coupled dependencies. That methodology is Dependency Injection (DI) and in spite of its intimidating name and underlying concepts, it’s actually very simple. Consider this code example:
The definition of SqlDataStoreProvider, DbLoggingProvider and ProdWebServiceProvider are empty, so I didn’t include them here for the sake of clarity. But they are supplied in the gist if you choose to download it.
The code above probably looks very familiar. Certainly you’ve no doubt written line of business applications in .NET that have required a data store. Many companies require logging of transactions, which would require a logging component of some kind. With the popularity of distributed computing the need to use web services is also common. Many .NET applications, especially old ones, often use code similar to this to create instances of the classes that they are dependent on to bind to. The problem with this approach is that the BusinessService class is tightly, or statically bound to specific implementations. This makes the application brittle and results in a situation where a seemingly innocuous change to a class can have far reaching and sometimes unpredictable consequences. It also sacrifices the ability to defer the decision of what kind of concrete object I want to bind to, meaning I am stuck with whatever component is being used at compile-time.
What DI does is remove the static binding of dependencies buy having the consumer of the class provide implementations that it wants the object to use:
By injecting my classes dependencies through a constructor I can control what concrete implementation actually gets used at runtime; the concrete implementation of my dependency just needs to be based on the abstraction that the consuming class (BusinessService) requires. For example, I can pass in any class that inherits from DataStoreProvider (like a SqlDataStoreProvider) to BusinessService via its constructor and the class can consume it. The methodology also keeps my code flexible and more tolerant of change. Since the class is only dependent on a base abstraction of a dependency I can make changes to the derived class and the consuming class will still function.
Putting your dependencies in an Inheritance based class hierarchy is valid approach to using DI to create loosely bound dependencies, and this type of class relationship certainly makes it easier to leverage reuse by implementing common code in the base class. However as interfaces are not dependent on a hierarchical relationship they can provide a more flexible way to define a common abstraction for our concrete implantations. If I were to refactor my BusinessService again to be reliant on an interface (aka, a shared public API) I would gain extra flexibility in my bindings. The resulting code would look something like this:
Now my BusinessService only cares that the concrete implementations that are provided have the same public API that is defined by the appropriate interface.
Dependency Injection is a simple yet powerful technique. It enables you to create loosely couple applications by differing the binding of our applications dependencies until runtime. Additionally we can specify our dependencies as abstractions and actually bind to a concrete implementation that is based on that abstraction. This keeps our software flexible and open to change. In the next post I’ll demonstrate the use of software factories and DI frameworks to help manage our dependencies and make DI even easier to use.
Continue the TDD journey:
Subscribe to be the first to get our expert-written articles and tutorials for developers!