SOLID — Dependency Inversion Principle (with C# example)

Alex
6 min readDec 7, 2023

What is the Dependency Inversion Principle?

The Dependency Inversion Principle (DIP) explains that a high-level class must not depend upon a lower-level class. They both must depend on abstractions and abstractions must not depend on details, but the details must depend upon abstractions. Ensuring the above will mean the application is robust, easy to maintain and expandable. Dependency Inversion is closely tied to the other principles Interface Segregation and Open-Closed Principle.

What is a high/low level module?

High level modules usually describe operations in our applications that contain the core business logic, whereas, the low-level modules contain the implementation details such as data storage and detail the more specific/modularised code which are used within the high-level classes.

Benefits of implementing DIP

A major benefit of implementing the DIP is the reduction of coupling between components. If we depend on abstractions then we can create a modular architecture which will allow us to replace and modify low-level implementations without affecting the high-level modules. This implementation allows for the building of a system that can handle changes in requirements gracefully.

By implementing DIP we also ensure that the C# code is organized and decoupled so it is easier to modufy and extend and also has a clean separation of concerns so our code is easier to read.

C# Example (Not implementing DIP)

Create a console application in your chosen IDE and create a class at the root of the project called Student.cs.

public class Student
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int YearOfBirth { get; set; }
}

In addition, we need to create another file at the root of the project called StudentDataAccessLogic.cs which will just contain a method that passes in an id to return an instance of a student, in a real application this would be where your database access logic would be.

public class StudentDataAccessLogic
{
public Student GetStudentDetails(int id)
{
Student student = new Student()
{
ID = 1,
FirstName = "John",
LastName = "Doe",
YearOfBirth = 1990
};

return student;
}
}

Furthermore, we will need to create a class called DataAccessFactory.cs which will contain a static method that returns a new instance of the StudentDataAccessLogic.cs class. When we create a new instance of this class in another file we will have access to the GetStudentDetails method, however, this is not the way we should be implementing this it is just an example of how the code will change before DIP implementation.

public class DataAccessFactory
{
public static StudentDataAccessLogic GetStudentDataAccess()
{
return new StudentDataAccessLogic();
}
}

We will now create a file at the root of the project called StudentBusinessLogic.cs which will contain the code in which we make an instance of the StudentDataAccessLogic class.

public class StudentBusinessLogic
{
private StudentDataAccessLogic _studentDataAccessLogic;

public StudentBusinessLogic()
{
_studentDataAccessLogic = DataAccessFactory.GetStudentDataAccess();
}

public Student GetDetails(int id)
{
return _studentDataAccessLogic.GetStudentDetails(id);
}

}

As you can see from the code above we are creating a variable of type StudentDataAccessLogic called _studentDataAccessLogic and setting it equal to a new instance of GetStudentDataAccess found in the DataAccessFactory class in the constructor of the class and then within a method called GetDetails in we access the method named GetStudentDetails found in the StudentDataAccessLogic class.

public interface IStudentDataAccessLogic
{
Student GetDetails(int id);
}

If you remember from earlier the explaination regarding high/low-level module, a high-level module is one that depends on other modules, so from the code above we can see that StudentBusinessLogic is depending on the StudentDataAccessLogic class. So the StudentBusinessLogic class is the high-level module and StudentDataAccessLogic is the low-level module.

So when referring back to the DIP the StudentBusinessLogic shouldn’t depend on a concrete StudentDataAccessLogic instance, instead both classes should depend on an abstraction of the class. So from that description we can say that the DIP is not being followed.

Abstraction

Abstraction in programming terms can mean non-concrete. So to implement DIP we must create an interface or an abstract class (something that is non-concrete) so we can’t create an instance of it.

If we look back at our code we can see that StudentBusinessLogic is dependent on StudentDataAccessLogic and that StudentBusinessLogic uses the method GetStudentDetails. In a real-world scenario you would have more than just the one method so we need to define an interface to contain the definition for this method or an abstract class. One thing to take note of is that by default interface methods are abstract, however, if you use an abstract class then you must declare the methods as abstract.

For this example I will be using an interface, so in the root of your application create an interface file called IStudentDataAccessLogic.cs which will contain the definition of the GetDetails method so any class that implements this interface will have to implement their own version of the GetDetails method.

We will do this with the StudentDataAccessLogic class.

public class StudentDataAccessLogic : IStudentDataAccessLogic
{
public Student GetDetails(int id)
{
Student student = new Student()
{
ID = 1,
FirstName = "John",
LastName = "Doe",
YearOfBirth = 1990
};

return student;
}
}

We now need to change the DataAccessFactory class and the return type of the static method to be the interface we just created. This method creates an instance of the StudentDataAccessLogic class, however, we return that instance using the parent interface i.e. IStudentDataAccessLogic. This functionality is possible because a parent class reference variable can contain the child class object reference. In this example IStudentDataAccessLogic is the parent and StudentDataAccessLogic is the child.

public class DataAccessFactory
{
public static IStudentDataAccessLogic GetStudentDataAccess()
{
return new StudentDataAccessLogic();
}
}

Meanwhile, we now need to change the StudentBusinessLogic class to use the newly created interface instead of the concrete class StudentDataAccessLogic.

public class StudentBusinessLogic
{
private IStudentDataAccessLogic _studentDataAccessLogic;

public StudentBusinessLogic()
{
_studentDataAccessLogic = DataAccessFactory.GetStudentDataAccess();
}

public Student GetStudentDetails(int id)
{
return _studentDataAccessLogic.GetDetails(id);
}

}

That is it in terms of writing code that implements the DIP. The high-level module which is StudentBusinessLogic and low-level module which is StudentDataAccessLogic depend on abstraction through the IStudentDataAccessLogic. In addition, abstraction doesn’t depend on details (StudentDataAccessLogic), but the details depend on abstraction.

Now open the Program.cs file and paste the below code.

StudentBusinessLogic studentBusinessLogic = new StudentBusinessLogic();

Student student = studentBusinessLogic.GetStudentDetails(1);

Console.WriteLine($"Student information: ID -{student.ID} , First name - {student.FirstName} , Last name - {student.LastName}, Birth year - {student.YearOfBirth} ");

You should see the below output.

The StudentBusinessLogic and StudentDataAccessLogic class are loosely coupled meaning they are not dependent on each other so if we make a change to one class it won’t affect the other because StudentBusinessLogic doesn’t depend on StudentDataAccessLogic as it has a reference to the interface IStudentDataAccessLogic. Meaning that we can use or create another class that implements the IStudentDataAccessLogic interface and we do not have to change StudentBusinessLogic therefore adhering to the DIP.

Thank you very much for reading my article I hope you enjoyed it and don’t forget to clap, follow and share my article!

References

--

--