The dependency injection light bulb moment
If, after reading this short article, you will get the kind of light bulb moment as I did, I have done my job correctly. I`m going to show you some simple code, that might help you understand the gist of dependency injection (in .NET Core at least) in under 10 minutes.
Why use it?
4 reasons are usually listed:
- Testability - this is probably the most important reason
- Inversion of control - meaning your objects will no longer be tightly coupled
- It`s the way the things are done right now - it`s one of the corner stones of .NET Core
- Coolness element - may not be important for some devs, but the feeling that you get, when you refactor old code in the way, that makes it testable, decoupled, and clean can be reward in itself. For a minute you feel like you actually know what you are doing.
The code
Lets create a super simple app, that has 2 classes: Car, Engine. Both have one method: Start. That writes to console. I will show you two ways to do it, one without DI, one with proper DI in place. First, I will show you how not to do it, run your dotnet new console command and replace code in Program CS with:
namespace DI
{
class Car
{
public void StartTheCar()
{
Engine engine = new Engine();
System.Console.WriteLine("Unlocked the car.");
engine.Start();
}
}
class Engine
{
public void Start()
{
System.Console.WriteLine(""Brm brrrrm..."");
}
}
class Program
{
static void Main(string[] args)
{
Car someCar = new Car();
someCar.StartTheCar();
}
}
}
Now run it. Beautiful! It works! But I want to unit test the StartTheCar() method alone. Ho do I do it? I dont, because Car class has a dependency instantiated inside the method. What can we do about it? Maybe we can create the engine instance in the Main method, and pass it to the Car, when calling the method like this:
namespace DI
{
class Car
{
public void StartTheCar(Engine engine)
{
System.Console.WriteLine("Unlocked the car.");
engine.Start();
}
}
class Engine
{
public void Start()
{
System.Console.WriteLine(""Brm brrrrm..."");
}
}
class Program
{
static void Main(string[] args)
{
Car someCar = new Car();
Engine engine = new Engine();
someCar.StartTheCar(engine);
}
}
}
Again, it works! Hell, we can even unit test it. But in reality we will have more methods in Car class, than Start. We will have: Start, Accelerate, Break, TurnOnTheRadio, Stop, etc... We don`t want (most of the time) to pass an instance to each method. Dependencies that are not optional (like the engine for car, or db context for data controller) should be instantiated in constructor of the class. So we will do:
namespace DI
{
class Car
{
Engine _Engine;
public Car(Engine engine)
{
_Engine = engine;
}
public void StartTheCar()
{
System.Console.WriteLine("Unlocked the car.");
_Engine.Start();
}
}
class Engine
{
public void Start()
{
System.Console.WriteLine(""Brm brrrrm..."");
}
}
class Program
{
static void Main(string[] args)
{
Engine engine = new Engine();
Car someCar = new Car(engine);
someCar.StartTheCar();
}
}
}
Good enough, but to properly test our car we want our car to accept any engine (PetrolEngine, DieselEngine, ...) that implements 'void Start()' method. You are probable thinking INTERFACE now, and you are right. Lets do it. And while we are at it, let`s also create an interface for Car, because you never know when you might need it. This is also a good time to separate our classes and interfaces to files, to make program look cleaner:
//IEngine.cs
namespace DI
{
interface IEngine
{
void Start();
}
}
//ICar.cs
namespace DI
{
interface ICar
{
void StartTheCar();
}
}
//Car.cs
namespace DI
{
class Car : ICar
{
public IEngine _Engine { get; set; }
public Car(IEngine engine)
{
_Engine = engine;
}
public void StartTheCar()
{
System.Console.WriteLine("Unlocked the car.");
_Engine.Start();
}
}
}
//Engine.cs
namespace DI
{
class Engine : IEngine
{
public void Start()
{
System.Console.WriteLine("Brm brrrrm...");
}
}
}
//Program.cs
namespace DI
{
class Program
{
static void Main(string[] args)
{
IEngine engine = new Engine();
ICar someCar = new Car(engine);
someCar.StartTheCar();
}
}
}
Again, this is amazing, but we have to create engine instance
(dependency), and pass it in the car constructor (first, and second line
in the Main method). That might seem OK now, but imagine we have a
larger dependency chain. We would have to manually instantiate each
dependency inside constructor of another dependency etc... It can become
a nested nightmare very quickly. So our last step will be to remedy
this, pull up your sleeve and add DependencyInjection package like this:
dotnet add package Microsoft.Extensions.DependencyInjection
. Now
change your Program.cs:
using Microsoft.Extensions.DependencyInjection;
namespace DI
{
class Program
{
static void Main(string[] args)
{
//initializing services
var services = new ServiceCollection()
.AddTransient<IEngine, Engine>()
.AddTransient<ICar, Car>()
.BuildServiceProvider();
//using services
var car = services.GetService<ICar>();
car.StartTheCar();
}
}
}
Look at it. Run it. Now the mentioned light bulb should go off in your head. How cool is that?! "No manual class instantiation", you just get the car service, and it fills the engine dependency on the go! Add any amount of services, nest them to your liking. As long as you have everything
- Constructor injection
- Interfaces, and
- ServiceCollection
set up, you`re good to go. Your code is now ready for testing, and in a whole different level of quality, compared to the first example.