However this is at compile time that the dependency injection occurs.
The service type is defined at compile time. This is normally in the form of an interface or a (sometimes abstract) base class. The key point here is Liskovs substitution principle
It states that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may be substituted for objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.)
So we know the contract of the type at compile time
Then what does it mean that dependency injection also occurs at Runtime?
When implementing DI, for example, using an Inversion of Control container the implementation type is not known until run time.
Let's look at a basic example of making a cup of coffee using Simple Injector as the Inversion of Control container and NUnit to test the code.
Firstly we define the cup and our service type:
public class Cup
{
public List<string> ingredients = new List<string>();
}
public interface ICoffeeService
{
Cup GetCoffee();
}
And an implementation of the service:
public class CoffeeService : ICoffeeService
{
public Cup GetCoffee()
{
Cup cup = new Cup();
cup.ingredients.Add("Coffee powder");
cup.ingredients.Add("Hot water");
return cup;
}
}
Our test method configures the container to return the CoffeeService
when asked for an instance of ICoffeeService
[Test]
public void CoffeeService_GetCoffee_ReturnsCupWithCoffeeAndHotWater()
{
// Arrange
var container = new SimpleInjector.Container();
container.Register<ICoffeeService, CoffeeService>();
// Act
var coffeeService = container.GetInstance<ICoffeeService>();
var cup = coffeeService.GetCoffee();
// Assert
Assert.That(cup.ingredients.Count == 2);
Assert.That(cup.ingredients.Contains("Coffee powder"));
Assert.That(cup.ingredients.Contains("Hot water"));
}
We decide we need to always add cream to our coffee but instead of changing CoffeeService
we instead Decorate it with the new feature (Open/Closed principle).
The decorator
public class AddCreamDecorator : ICoffeeService
{
private readonly ICoffeeService decorated;
public AddCreamDecorator(ICoffeeService decorated)
{
this.decorated = decorated;
}
public Cup GetCoffee()
{
Cup cup = this.decorated.GetCoffee();
cup.ingredients.Add("Cream");
return cup;
}
}
And the new test
[Test]
public void DecoratedCoffeeService_GetCoffee_CupWithCoffeeAndHotWaterAndCream()
{
// Arrange
var container = new SimpleInjector.Container();
container.Register<ICoffeeService, CoffeeService>();
container.RegisterDecorator(typeof(ICoffeeService), typeof(AddCreamDecorator));
// Act
var coffeeService = container.GetInstance<ICoffeeService>();
var cup = coffeeService.GetCoffee();
// Assert
Assert.That(cup.ingredients.Count == 3);
Assert.That(cup.ingredients.Contains("Coffee powder"));
Assert.That(cup.ingredients.Contains("Hot water"));
Assert.That(cup.ingredients.Contains("Cream"));
}
The most important point to take from this example is that we code to and compile against the service type ICoffeeService
and the container gives us the configured runtime implementation. In test one we get an instance of CoffeeService
while in test two we get an instance of CoffeeService
that is decorated with an instance of AddCreamDecorator
- in both cases the caller doesn't care as it get the service it needs.