Skip to content

Latest commit

 

History

History
344 lines (253 loc) · 13.9 KB

README.md

File metadata and controls

344 lines (253 loc) · 13.9 KB

DI.WeatherApp

[SOLID] Dependency Inversion. What? How?

.net build workflow License: MIT Generic badge

🧑‍🎓 Disclaimer:

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.

Dependency Inversion

🧠 Definition

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.

The 🎯 of dependency inversion principle

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.

🥇 Golden rule

High-level (client) modules should own the abstraction otherwise the dependency is not inverted!

🚀 Summary

  • 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

🧰 Demo

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.

console-ui

DI.WheatherApp is a simple demo project. It's organized like this:

User Interface Layer

DI.WeatherApp.ConsoleClient

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

DI.WeatherApp.Services

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

DI.WeatherApp.Data

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.

not-inverted-dependency

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.

So what?!

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

changed-property-name

When we build the project we get the following errors.

build-errors

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.

  1. ✔️ Change interface ownership.

    The IDummyWeatherForecastRepository should be owned by the DI.WeatherApp.Services instead of the DI.WeatherApp.Data Also the IEnumerable<WeatherForecastDbo> Get(); should now return IEnumerable<WeatherForecast> Get(); because this is all we need in the business layer.

    change-interface-ownership

  2. ✔️ Invert the dependency.

    Remove project reference to DI.WeatherApp.Data from DI.WeatherApp.Services and add a reference to DI.WeatherApp.Services in DI.WeatherApp.Data to start using the IDummyWeatherForecastRepository interface. We also need to move the WeatherForecastDbo mapping to WeatherForecast in the DummyWeatherForecastRepository because now it is responsible for returning WeatherForecast data.

    invert-project-dependencies

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

    interface-implementation

  3. ✔️ Register all dependencies in the UI layer.

    In our case, the UI layer is the composition route for all dependencies.

    register-data-layer-in-ui

    ⚖️ Without dependency injection we can not achieve dependency inversion.

    dependency-injection-configuration

🎉 Did we invert the dependency? 🎉

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.

inverted-dependency

This is what the dependency flow looks like

inverted-dependency-flow-chart

🎉 We can make changes to the lower level DI.WeatherApp.Data project, and this won't affect higher

🔗 Check out this PR to see what was changed so that the project is following dependency inversion principle