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:
- Single Responsibility Principle (SRP)
- Open-Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
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.