C# Dependency Inversion Principle

Summary: in this tutorial, you’ll learn about the C# Dependency Inversion Principle that promotes the decoupling of software modules.

Introduction to the C# Dependency Inversion Principle

The Dependency Inversion Principle (DIP) is the last principle of the SOLID principles:

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules, but both should depend on abstractions.

To understand the Dependency Inversion Principle, you first need to understand the concepts of high-level and low-level modules.

In general, high-level modules are modules that contain the main logic of the application, while low-level modules are modules that provide supporting functionality. In other words, high-level modules specify what the application will do while the low level-modules specify how the application will do it.

Traditionally, low-level modules depend on high-level modules, as the high-level modules call the low-level modules to perform their required functionality. However, this creates a tight coupling between the two modules. Therefore, it’s difficult to change one module without affecting the other.

The Dependency Inversion Principle solves this problem by introducing an abstraction layer between the high-level and low-level modules.

This abstraction layer is represented by an interface, which defines the methods that the low-level module must implement to provide its functionality. The high-level module depends on this interface instead of the low-level module. In other words, the Dependency Inversion Principle promotes the decoupling of software modules.

C# Dependency Inversion Principle example

The following example is an illustration of violating the dependency inversion principle:

namespace DIP;

public class DatabaseService
{
    public void Save(string message)
    {
        Console.WriteLine("Save the message into the database");
    }
}
public class Logger
{
    private readonly DatabaseService _databaseService;

    public Logger(DatabaseService databaseService)
    {
        _databaseService = databaseService;
    }
    public void Log(string message)
    {
        _databaseService.Save(message);
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        var logger = new Logger(new DatabaseService());
        logger.Log("Hello");
    }
}
Code language: C# (cs)

In this example, we have two main classes DatabaseService and Logger:

  • The DatabaseService is a low-level module that provides database access.
  • The Logger is a high-level module that logs data.

The Logger class depends on DatabaseService class directly. In other words, the high-level module (Logger) depends on the low-level module (DatabaseService).

Rather than having the Logger class depends on the DatabaseService class, we can introduce an interface called IDataService that both classes depend on:

namespace DIP;

public interface IDataService
{
    public void Save(string message);
}

public class DatabaseService: IDataService
{
    public void Save(string message)
    {
        Console.WriteLine("Save the message into the database");
    }
}

public class Logger
{
    private readonly IDataService _dataService;

    public Logger(IDataService dataService)
    {
        _dataService = dataService;
    }

    public void Log(string message)
    {
        _dataService.Save(message);
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        var logger = new Logger(new DatabaseService());
        logger.Log("Hello");
    }
}Code language: C# (cs)

In this example:

First, define the IDataAccess interface that has the Save() method:

public interface IDataService
{
    public void Save(string message);
}Code language: C# (cs)

Second, redefine the DatabaseAccess that implements the IDataAccess interface:

public class DatabaseService: IDataService
{
    public void Save(string message)
    {
        Console.WriteLine("Save the message into the database");
    }
}Code language: C# (cs)

Third, change the member and constructor of the Logger class to use the IDataAccess interface instead of the DatabaseAccess class:

public class Logger
{
    private readonly IDataService _dataService;

    public Logger(IDataService dataService)
    {
        _dataService = dataService;
    }

    public void Log(string message)
    {
        _dataService.Save(message);
    }
}Code language: C# (cs)

By doing this, we can decouple the Logger and DatabaseService classes, making it easier to change one class without affecting each other.

Also, we can easily swap the DatabaseService class for a different class that implements the IDataService interface. For example, we can define FileService class that saves a message into a text file by passing it to the Logger class.

Summary

  • The Dependency Inversion Principle states that high-level modules should not depend on low-level modules, but both should depend on abstractions.
Was this tutorial helpful ?