[SOLID] Dependency Inversion. What? How?
This summary on DI is based on my understanding of the DI principle (after research) and is for learning purposes. It's open for discussion and improvements. You can check out the demo source code below.
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
Is to avoid this highly coupled distribution with the mediation of an abstract layer and to increase the re-usability of higher/policy layers. It promotes loosely coupled architecture, flexibility, pluggability within our code.
High-level (client) modules should own the abstraction otherwise the dependency is not inverted!
- Dependency Inversion Principle- Higher-level component owns the interface. Lower-level implements.
- Dependency Injection Pattern - Frequently, developers confuse IoC with DI. As mentioned previously, IoC deals with object creation being inverted, whereas DI is a pattern for supplying the dependencies since the control is inverted.
- Inversion of Control - is theย techniqueย for inverting the control of object creation andย notย the actual patternย for making it happen.
- Dependency Inversion promotes loosely coupled architecture, flexibility, pluggability within our code
- Without Dependency injection, there is no Dependency inversion
This demo is for future references if/when I don't feel confident enough that I understand in practice what DI is all about. I think dependency inversion is best explained in simple N-layered architecture so I'll try doing just that.
DI.WheatherApp is a simple demo project. It's organized like this:
User Interface Layer
A simple console client to display dummy weather data. This represents the UI layer and orchestrates the dependency injection.
Startup.cs - Adds services to the DI container. Entry point for the console app.
static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureServices((_, services) => services.AddScoped<IWeatherService, WeatherService>() .AddScoped<IDummyWeatherForecastRepository, DummyWeatherForecastRepository>() .AddScoped<WeatherDataConsumer>()); static async Task Main(string[] args) { using IHost host = CreateHostBuilder(args).Build(); var weatherDataConsumer = host.Services.GetRequiredService<WeatherDataConsumer>(); weatherDataConsumer.Display(); await host.RunAsync(); }
WeatherDataConsumer.cs - simple console UI build with ConsoleTables and Humanize
public class WeatherDataConsumer { private readonly IWeatherService weatherService; /// <summary> /// Initializes a new instance of the <see cref="WeatherDataConsumer"/> class. /// </summary> /// <param name="weatherService">The weather service.</param> public WeatherDataConsumer(IWeatherService weatherService) { this.weatherService = weatherService; } /// <summary> /// Displays data on the console with the ConsoleTable and Humanize libraries /// </summary> public void Display() { var table = new ConsoleTable( nameof(WeatherForecast.CityName).Humanize(), nameof(WeatherForecast.Date).Humanize(), nameof(WeatherForecast.TemperatureC).Humanize(), nameof(WeatherForecast.TemperatureF).Humanize(), nameof(WeatherForecast.Summary).Humanize()); foreach (var forecast in this.weatherService.Get()) { table.AddRow(forecast.CityName, forecast.Date.ToString("ddd, dd MMM yyy"), forecast.TemperatureC, forecast.TemperatureF, forecast.Summary); } table.Write(); } }
At the moment the UI is only referencing <ProjectReference Include="..\DI.WeatherApp.Services\DI.WeatherApp.Services.csproj" />
Business Logic Layer
This represents the business layer.
WeatherService.cs - weather service that uses the dummy weather forecast repository to return data
public class WeatherService : IWeatherService { private readonly IDummyWeatherForecastRepository weatherForecastRepository; /// <summary> /// Initializes a new instance of the <see cref="WeatherService"/> class. /// </summary> /// <param name="weatherForecastRepository">The weather forecast repository.</param> public WeatherService(IDummyWeatherForecastRepository weatherForecastRepository) { this.weatherForecastRepository = weatherForecastRepository; } /// <inheritdoc/> public IEnumerable<WeatherForecast> Get() { return this.weatherForecastRepository.Get().Select(w => new WeatherForecast() { CityName = w.CityName, Date = w.Date, Summary = w.Summary, TemperatureC = w.TemperatureC }); } }
IWeatherService.cs - abstraction over the WeatherService class
WeatherForecast.cs - POCO holding weather data
public class WeatherForecast { /// <summary> /// Gets or sets the name of the city. /// </summary> public string CityName { get; set; } /// <summary> /// Gets or sets the date. /// </summary> public DateTime Date { get; set; } /// <summary> /// Gets or sets the temperature Celsius. /// </summary> public int TemperatureC { get; set; } /// <summary> /// Gets the temperature Fahrenheit. /// </summary> public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); /// <summary> /// Gets or sets the summary. /// </summary> public string Summary { get; set; } }
At the moment the Business Layer is only referencing <ProjectReference Include="..\DI.WeatherApp.Services\DI.WeatherApp.Data.csproj" />
Data access Layer
Represents the data access layer.
DummyWeatherForecastRepository.cs - dummy weather data repository
public class DummyWeatherForecastRepository : IDummyWeatherForecastRepository { #region Private fields private static readonly string[] Summaries = new[] { "Warm", "Bring an umbrella", "Chilly", "Freezing" }; private static readonly int[] Temperatures = new[] { 20, 10, 5, -4 }; private static readonly string[] CityNames = new[] { "Sofia", "London", "New York", "Brisbane", "Novosibirsk" }; #endregion /// <inheritdoc/> public IEnumerable<WeatherForecastDbo> Get() { var random = new Random(); return Enumerable.Range(1, CityNames.Length - 1) .Select(i => { var randomIndex = random.Next(Summaries.Length); return new WeatherForecastDbo { CityName = CityNames[i], Date = DateTime.Now.AddDays(1), Summary = Summaries[randomIndex], TemperatureC = Temperatures[randomIndex] }; }) .ToArray(); } }
IDummyWeatherForecastRepository.cs - abstraction over the DummyWeatherForecastRepository class
WeatherForecastDbo.cs - Weather Data Dbo
public class WeatherForecastDbo { /// <summary> /// Gets or sets the name of the city. /// </summary> public string CityName { get; set; } /// <summary> /// Gets or sets the date. /// </summary> public DateTime Date { get; set; } /// <summary> /// Gets or sets the temperature in Celsius. /// </summary> public int TemperatureC { get; set; } /// <summary> /// Gets or sets the summary. /// </summary> public string Summary { get; set; } }
Altho we have some dependency injection in this setup, we are not inverting any dependencies thus not implementing the dependency inversion principle.
As shown in the graph, the dependency is not inverted and the UI is depending on the Business layer, which depends on Data layer. This means that the higher-level layer depends on the implementation details of the lower-level layer. We're trying to use an interface to decouple this dependency but this is not enough.
To demonstrate an issue with not inverting dependencies we could introduce a change in the Data layer. Let's say that we need to change a property name in the WeatherForecastDbo.cs
The issue here is that this error is now reflecting an outside project in the business layer. This means, that if we need to rename a property in our database, this would affect the business logic. We could think, that after we're using an interface (abstraction) we're safe from such things, but we didn't actually invert the dependencies.
To fix this, we need to follow the simple rule - the client (higher modules) own the interface.
-
The IDummyWeatherForecastRepository should be owned by the DI.WeatherApp.Services instead of the DI.WeatherApp.Data Also the
IEnumerable<WeatherForecastDbo> Get();
should now returnIEnumerable<WeatherForecast> Get();
because this is all we need in the business layer. -
Remove project reference to
DI.WeatherApp.Data
fromDI.WeatherApp.Services
and add a reference toDI.WeatherApp.Services
inDI.WeatherApp.Data
to start using theIDummyWeatherForecastRepository
interface. We also need to move theWeatherForecastDbo
mapping to WeatherForecast in theDummyWeatherForecastRepository
because now it is responsible for returningWeatherForecast
data.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
-
In our case, the UI layer is the composition route for all dependencies.
To verify that dependency was indeed inverted, we will introduce a change in the Data layer like before.
๐ This time the error will be contained only in the DI.WeatherApp.Data
and we don't need to update higher-level modules.