Tutorials / Build a REST API with C# and .NET · Lesson 4 of 15
On this page
Dependency Injection Basics
Why interfaces matter
An interface is a list of what something can do, without saying how it does it. It is a contract.
Interfaces give you two benefits:
- You focus on what you need, and trust that whatever fulfils the contract does its job. You do not need to know the details, which leaves far less to think about.
- On a team, you can agree on the interface first. Then you work on your class while a coworker builds the thing behind the interface, in parallel, without waiting for each other.
Two everyday analogies
- A milk tea cup. You make milk tea. You do not care how the cup is made or from what. You only agree with your supplier on how tall and wide it is and that it must not melt when it holds a hot drink. You focus on the tea, and the supplier focuses on cups. The agreement is the contract.
- An electric fan. You plug a fan into the wall. You do not care whether the power company uses solar, wind or coal, or what wiring is behind the wall. You trust the socket: plug in, get power.
Now let's see it in code. Say you are building a dice game. A die cannot always show 6, so you need a random number from 1 to 6. You can hand that job to an interface:
public interface INumberGenerator
{
int GetDiceNumber(); // returns 1 to 6
}
Your coworker builds the part behind the contract:
public class ClockNumberGenerator : INumberGenerator
{
public int GetDiceNumber()
{
long ticks = DateTime.Now.Ticks;
return (int)((ticks * 31) % 6) + 1;
}
}
(This is a deliberately naive example. Do not use the clock for random numbers in real code. We will see why in a moment.)
You do not even need to read that code. You ask for a dice number and you get one you can trust, like the cup and the socket. You focus on the game: handling bets, deciding who wins, updating the money.
Interfaces make change painless
Here is something you may have lived through: you are halfway through building something, and the client or management wants a change. The team gets frustrated, because changing code mid-way is painful.
But change is not avoidable. Clients learn more about the real world. A competitor ships something better. Management finds a cheaper way to run the product. Change is guaranteed, so the useful mindset is to expect that people will change their minds, and to build so that changes are cheap. Interfaces are a big part of how the industry does that.
Example: the dice game. Suppose you had not used an interface, and hard-coded the clock-based code inside the dice game itself:
public int GetDiceNumber()
{
long ticks = DateTime.Now.Ticks;
return (int)((ticks * 31) % 6) + 1;
}
Later someone works out that the clock can be predicted. Now one player always wins, and your company is losing money. If that code is scattered across your games, fixing it means hunting it down everywhere. And what if other games also need a dice number?
With an interface, you change only the class behind it, and everything that uses the contract is fixed at once:
using System.Security.Cryptography;
public class SecureNumberGenerator : INumberGenerator
{
public int GetDiceNumber()
{
// Secure random number: 1 up to (not including) 7
return RandomNumberGenerator.GetInt32(1, 7);
}
}
No game code was touched. You fixed the generator, and every game is fixed.
Interfaces alone are not enough
Now imagine three dice games:
public class SimpleDiceGame
{
private INumberGenerator ng = new ClockNumberGenerator();
}
public class BoardGameDice
{
private INumberGenerator ng = new ClockNumberGenerator();
}
public class CasinoDiceGame
{
private INumberGenerator ng = new ClockNumberGenerator();
}
To switch generators, each change is small:
private INumberGenerator ng = new SecureNumberGenerator();
But you must make it in every game class. In a real system that could be 10 to 20 places, in several products. It is still a nightmare.
So today it is not good practice for a class to choose which implementation it uses. Instead, a class should receive it from outside, through its constructor (the special method that runs when an object is created):
public class SimpleDiceGame
{
private readonly INumberGenerator ng;
public SimpleDiceGame(INumberGenerator numberGenerator)
{
ng = numberGenerator;
}
// the rest of the game: bets, money accounts...
}
Now SimpleDiceGame never needs to be edited again, even when the generator changes:
- It is no longer tied to the clock-based generator.
- It does not know which implementation it received.
- It just expects someone to hand it a generator, and trusts that it works.
That leaves one question: if the game no longer decides, who does?
Who decides which implementation to use?
The answer: whoever builds the game. In a console app, that is Program.cs, the one file where everything gets put together:
INumberGenerator ng = new ClockNumberGenerator();
var simpleGame = new SimpleDiceGame(ng);
var boardGame = new BoardGameDice(ng);
var casinoGame = new CasinoDiceGame(ng);
The decision, "use ClockNumberGenerator", happens in exactly one place. None of the three game classes mention ClockNumberGenerator anywhere. They only know they were handed something that fulfils INumberGenerator.
Now the hacker problem from earlier: the clock turned out to be predictable, so we switch to SecureNumberGenerator. Here is the whole fix:
INumberGenerator ng = new SecureNumberGenerator();
var simpleGame = new SimpleDiceGame(ng);
var boardGame = new BoardGameDice(ng);
var casinoGame = new CasinoDiceGame(ng);
One line changed. The three game files are never opened. They never knew how the old generator worked, so they are unaffected by how the new one works.
So what is Dependency Injection?
That is the whole idea. A dependency is anything a class needs in order to do its job. SimpleDiceGame depends on a number generator.
When a class needs other classes to do its job, it should not choose them itself. It should be given them.
Why? Because somewhere in the future, someone will change their mind, and that is guaranteed. If your class never commits to one implementation, it may never need editing again, even when the change comes.
Dependency Injection (often shortened to DI) means that the decision about which dependency to use is made by another piece of code, not by the class itself. "Injection" just means "handing it in", here through the constructor.
Here is the whole idea in one file, Program.cs, where the app starts:
// Program.cs
INumberGenerator ng = new SecureNumberGenerator();
var simpleGame = new SimpleDiceGame(ng);
var boardGame = new BoardGameDice(ng);
var casinoGame = new CasinoDiceGame(ng);
// run the games...
- This is the only file that ever writes
new SecureNumberGenerator(). - Every game just says "give me an
INumberGenerator" and trusts what it is handed. - When someone changes their mind tomorrow, whether that is a different generator or a fake one for testing, this is the only file that changes.
Questions to think about
Before the next lesson, try answering these in your own words. There are no answers given here on purpose. If you can explain them out loud to a friend, you are ready to move on.
-
What is an interface? Think of an everyday example of your own, like the milk tea cup, and explain the contract in it.
-
What is a dependency? A
ReceiptPrinterclass needs something that prints text. What is its dependency? -
This class has a problem. What is it, and how would you fix it?
public class OrderService { private IPaymentProvider _payments = new CardPaymentProvider(); } -
The bookshop wants to try a fake payment provider while testing. With the fix from question 3, which file decides which provider is used? How many other files need to change?
-
The lesson says change is guaranteed. How does Dependency Injection make that easier to live with?
In the next lessons we look at what happens as the number of classes grows, and how .NET can do this handing-in for you.