This post is part of the series SOLID Wash Tunnel.

Definition

"Dependency injection makes a class independent of its dependencies. It achieves that by decoupling the usage of an object from its creation."

- Stackify

Dependency injection is used to implement the Dependency Inversion and Inversion of Control principles. It involves 3 types of players:

  • Dependent - A class which depends on some dependency.
  • Dependency - A class which offers some kind of service. This is often served via an abstraction to decouple the dependent from a concrete implementation of the dependency.
  • Injector - A class/component/framework which injects a dependency, into the dependent.
    Our custom IoC Container is considered to be an injector.

Dependency Injection Flow.


Types of DI

As stated above the injector injects the dependency into the dependent class. This can be achived in 3 ways:

  • Constructor Injection - The injector supplies the dependency through the dependent class constructor.
  • Method Injection - The injector supplies the dependency through any method of the dependent class, which accepts said dependency/abstraction.
  • Property Injection - The injector supplies the dependency through a public property of the dependent class.

Our custom IoC Container only supports constructor injection, so we will be using that throughtout the project.


Implementation

Dependency Injection is used across the whole code base!

For example lets elaborate the individual parts the PriceCalculator class, and relate them to the definitions made above for DI.

  • PriceCalculator - Is the dependent since it needs the ICurrencyRateConverter to do its work.
  • ICurrencyRateConverter - Is the dependency or more precisely the abstraction that is injected via constructor injection. It offers rate conversion functionality that the PriceCalculator depends on.
  • Our IoC container (Not shown here) - Is the injector since it supplies the PriceCalculator with an instance of ICurrencyRateConverter.
public interface IPriceCalculator
{
int Discount { get; }
Money Calculate(IWashProgram program, Currency currency);
}

public abstract class PriceCalculator : IPriceCalculator
{
protected readonly ICurrencyRateConverter converter;

public PriceCalculator(ICurrencyRateConverter converter)
{
this.converter = converter;
}

public virtual Money Calculate(IWashProgram program, Currency currency)
{
Money totalPrice = program
.GetWashSteps()
.Select(x => x.Price)
.Aggregate((x, y) => x + y);

return converter.Convert(totalPrice, currency);
}
}

public class IndividualPriceCalculator : PriceCalculator
{
public override int Discount => 0;

public IndividualPriceCalculator(ICurrencyRateConverter converter)
: base(converter) { }
}

public class CompanyPriceCalculator : PriceCalculator
{
public override int Discount => 20;

public CompanyPriceCalculator(ICurrencyRateConverter converter)
: base(converter) { }

public override Money Calculate(IWashProgram program, Currency currency)
{
Money totalPrice = base.Calculate(program, currency);
return totalPrice - (Discount / 100m * totalPrice);
}
}

Principles


PrincipleAppliedExplanation
SRPDeals only with calculation of prices.
OCPDifferent calculations are performed by extending the base PriceCalculator class.
LSPYou can supply an IndividualPriceCalculator or CompanyPriceCalculator where a PriceCalculator is expected.
ISPPriceCalculator (the client) makes use of all the methods of IPriceCalculator.
DIPHigh level modules like PriceCalculator do not depend on the low level modules like CurrencyRateConverter, LegacyCurrencyRateConverter. They depended on the abstraction ICurrencyRateConverter.

Continue the series on Simple Factory.

If you found this article helpful please give it a share in your favorite forums 😉.
The solution project is available at GitHub.