Dependency injection (DI) is a simple practice that boosts the testability of code and reduces coupling between classes. There are three key flavors of dependency injection including (1) Constructor Injection, (2) Setter Injection, and (3) Interface injection. I typically lump flavors (2) and (3) together, although their implementations may vary slightly.
For a long time, I avoided constructor injection like the plague. I had seen others implement their classes using constructor injection and rolled my eyes and said “I don’t understand why they would go on and do that…”. And that was exactly right. I didn’t understand constructor injection. In fact, I really didn’t understand dependency injection as a whole.
Dependency Injection Gone Wrong
Let’s rewind to how things were in our code base before trying to introduce the dependency injection concepts. To set the stage, let’s say we have some class Foo
that depends on another class Bar
. In the constructor of Foo,
we may instantiate a Bar
instance to use later on when Foo
needs it. These examples are in C#, but the concepts apply to any language:
In the land of Dependency Injection, the keyword new
should be somewhat of a red flag (unless instantiating a simple Data Transfer Object (DTO) with no behavior). This means we are coupling ourselves with a specific implementation of IBar,
namely Bar
, which is not ideal. Switching IBar
implementations is not quite that simple and impacts Foo
and any Foo
unit tests. Our team acknowledged that this was not good, so we figured we would adapt our code to make use of an Inversion of Control (Ioc) Container that we had inherited in our code base. This container was nothing more than a singleton wrapper around the Microsoft Unity DI container.
So we updated our constructor to make use of this container as follows:
This is better, in that now we don’t have any direct references to the Bar
implementation and are only aware of IBar.
However, we are now introducing another dependency, on the IocContainer.
This adds complexity to the unit tests because now each tests needs to configure the behavior of IocContainer
according to what is needed. This is also misusing the IocContainer
as a ServiceLocator,
which is not its intent.
We had our code in this state for a while, until I started to read into Angular.
Enter Angular2
Although I currently do very little web development, I was reading into Angular2 and watched a few Pluralsight courses that gave an intro into Angular2. One thing that was repeated was that any service can be accessed in a component by simply adding it to the constructor of the component, as shown below:
I was excited to share how awesome this was and went back to the office on Monday to discuss this with a colleague who uses Angular. Our conversation led to my understanding that this same behavior can be achieved using our our IocContainer and that we were greatly under-utilizing the IocContainer. In reality the IocContainer and Dependency Injection work together to automatically inject the dependencies. The IocContainer automatically resolves the types when instantiating the objects, so you never have to deal with invoking the constructor directly. This realization blew my mind, and I was so excited to test it out in some upcoming development. Sure enough, it worked!
So in all honesty, I can’t give Angular2 all the credit, but it certainly sparked the conversation.
Revisiting Constructor Injection
Up until this point I had been avoiding constructor injection because I thought it was ridiculous to require the user of your code to explicitly provide all the dependent implementations.
Let’s revisit the simple Foo example utilizing the IocContainer
appropriately. This assumes that the IocContainer
has been configured to register types for IFoo
and IBar:
Notice that there is no reference to the IocContainer
anywhere except for the application entry point. This is key and greatly increases testability of each component. Wherever possible, the IocContainer
direct usage should be as limited as possible, towards the outskirts of your application.
Another realization that I had at this point is that the flavor of dependency injection really didn’t matter so much, as long as the DI container supports it. With that said, I tend to favor constructor injection over setter/interface injection for two main reasons. First, it is extremely clear what the dependencies are, since they are listed in the constructor. Second, with setting/interface injection, the methods to inject the dependencies are public, meaning that the dependencies can be changed during the lifetime of the object. Unless this is required, I prefer to not have that option.
Conclusion
This corrected understanding has greatly improved my code and has simplified the unit tests. It is so easy to specify dependencies, and while you are developing a component, you don’t have to worry about where the dependencies are coming from.
Thank you Angular2 for embedding good dependency injection practices into the framework and for setting me straight on how I was using dependency injection and constructor injection!