Back to: SOLID Design Principles in C#
Dependency Inversion Principle in C# with Examples
In this article, I am going to discuss the Dependency Inversion Principle in C# with Examples. Please read our previous article before proceeding to this article, where we discussed the Interface Segregation Principle in C# with Examples. The Letter D in SOLID stands for the Dependency Inversion Principle, also known as DIP. You will understand the following pointers in detail at the end of this article.
- What is Dependency Inversion Principle in C#?
- Example without using Dependency Inversion Principle in C#
- Example using Dependency Inversion Principle in C#
- Advantages and Disadvantages of using the Dependency Inversion Principle in C#
- How to use Dependency Inversion Principle in C#?
What is the Dependency Inversion Principle in C#?
The Dependency Inversion Principle (DIP) states that High-Level Modules/Classes should not depend on Low-Level Modules/Classes. Both should depend upon Abstractions. Secondly, Abstractions should not depend upon Details. Details should depend upon Abstractions.
The most important point you need to remember while developing real-time applications is always to keep the High-level and Low-level modules as loosely coupled as possible.
When a class knows about the design and implementation of another class, it raises the risk that if we do, any changes to one class will break the other class. So we must keep these high-level and low-level modules/classes loosely coupled as much as possible. To do that, we need to make both of them dependent on abstractions instead of knowing each other. If this is not clear now, don’t worry; at the end of this article, you will understand this concept very well with examples.
Example to Understand Dependency Inversion Principle in C#
Let us understand Dependency Inversion Principle with one Example using C# Language. First, we will see the example without following the Dependency Inversion Principle. Then we will identify the problems of not following the Dependency Inversion Principle. Then we will rewrite the same example using the Dependency Inversion Principle so that you will understand this concept easily. First, create a Console Application and then add the following class files.
Employee.cs
Create a class file with the name Employee.cs and then copy and paste the following code into it. The following is a simple class having 4 properties. The following class is going to hold the employee data.
namespace SOLID_PRINCIPLES.DIP { public class Employee { public int ID { get; set; } public string Name { get; set; } public string Department { get; set; } public int Salary { get; set; } } }
EmployeeDataAccessLogic.cs
Create a class file with the name EmployeeDataAccessLogic.cs and then copy and paste the following code into it. The following class contains one method which takes the employee id and returns that Employee information. In a real-time application, you need to write the logic to get the employee details from the database, but for simplicity, we have hard-coded the employee details here. Also, in a real-time application, you might have more methods like getting all employee information, creating and updating employees, deleting employees, etc. Here, we have created only one method. So, this is the layer that will contain the logic to interact with the database, and hence we said this class as EmployeeDataAccessLogic.
namespace SOLID_PRINCIPLES.DIP { public class EmployeeDataAccessLogic { public Employee GetEmployeeDetails(int id) { //In real time get the employee details from database //but here we have hard coded the employee details Employee emp = new Employee() { ID = id, Name = "Pranaya", Department = "IT", Salary = 10000 }; return emp; } } }
DataAccessFactory.cs
Create a class file with the name DataAccessFactory.cs and then copy and paste the following code into it. The following class contains one static method, returning an instance of the EmployeeDataAccessLogic class. If you want to consume any method of the EmployeeDataAccessLogic class, then you need to create an instance of that class. In our example, the following class, GetEmployeeDataAccessObj() static method, is going to return an instance of the EmployeeDataAccessLogic class, and using that instance, we can access the GetEmployeeDetails(int id) method. So, this is the class that is going to return an instance of the EmployeeDataAccessLogic class, using which we can do the database operations.
namespace SOLID_PRINCIPLES.DIP { public class DataAccessFactory { public static EmployeeDataAccessLogic GetEmployeeDataAccessObj() { return new EmployeeDataAccessLogic(); } } }
EmployeeBusinessLogic.cs
Create a class file with the name EmployeeBusinessLogic.cs and then copy and paste the following code into it. The following class has one constructor that is used to create an instance of the EmployeeDataAccessLogic class. Here, within the constructor, we call the static GetEmployeeDataAccessObj() method on the DataAccessFactory class, which will return an instance of EmployeeDataAccessLogic, and we initialize the _EmployeeDataAccessLogic property with the return instance. We also have one method, i.e., GetEmployeeDetails, which is used to call the GetEmployeeDetails method on the EmployeeDataAccessLogic instance to get the employee detail by employee id.
namespace SOLID_PRINCIPLES.DIP { public class EmployeeBusinessLogic { EmployeeDataAccessLogic _EmployeeDataAccessLogic; public EmployeeBusinessLogic() { _EmployeeDataAccessLogic = DataAccessFactory.GetEmployeeDataAccessObj(); } public Employee GetEmployeeDetails(int id) { return _EmployeeDataAccessLogic.GetEmployeeDetails(id); } } }
Comparing the above Example with Dependency Inversion Principle in C#
As per the Dependency Inversion Principle definition, “a High-Level module should not depend on Low-Level modules. Both should depend on the abstraction”.
So, first, we need to figure out the High-Level Module (class) and the Low-Level Module (class) in our example. A High-Level Module is a module that always depends on other modules. So, in our example, the EmployeeBusinessLogic class depends on the EmployeeDataAccessLogic class, so here, the EmployeeBusinessLogic class is the high-level module, and EmployeeDataAccessLogic class is the low-level module.
So, as per the first rule of the Dependency Inversion Principle in C#, the EmployeeBusinessLogic class/module should not depend on the concrete EmployeeDataAccessLogic class/module. Instead, both classes should depend on abstraction. But, in our example, the way we have implemented the code, the EmployeeBusinessLogic depending on the EmployeeDataAccessLogic class, means the first rule we are not following. Later part of this article, I will modify the example to follow Dependency Inversion Principle.
The second rule of the Dependency Inversion Principle state that “Abstractions should not depend on details. Details should depend on Abstractions”. Before understanding this, let us first understand what is an abstraction.
What is Abstraction in C#?
In simple words, we can say that Abstraction means something which is Non-Concrete. So, Abstraction in Programming means we need to create either an Interface or an Abstract Class, which is Non-Concrete, so that we can not create an instance of it. In our example, the EmployeeBusinessLogic and EmployeeDataAccessLogic are concrete classes, meaning we can create objects of them. That means we are also not following the second rule of the Dependency Inversion Principle.
As per the Dependency Inversion Principle in C#, the EmployeeBusinessLogic (High-Level Module) should not depend on the concrete EmployeeDataAccessLogic (Low-Level Module) class. Both classes should depend on Abstractions, meaning both classes should depend on either an Interface or an Abstract Class.
What should be in Interface (or in Abstract Class)?
As you can see in the above example, EmployeeBusinessLogic uses the GetEmployeeDetails() method of the EmployeeDataAccessLogic class. In real-time, there will be many employee-related methods in the EmployeeDataAccessLogic class. So, we need to declare the GetEmployeeDetails(int id) method or any employee-related methods within the interface or abstract class. By default, interface methods are going to be abstract. But if you create an abstract class, you need to explicitly declare the methods as abstract by using the abstract keyword. I am going with Interface as it makes the code more loosely coupled, and we can achieve multiple inheritances.
IEmployeeDataAccessLogic.cs
Create a class file with the name IEmployeeDataAccessLogic.cs and then copy and paste the following code into it. As you can see, here we created the interface with one abstract method, i.e., GetEmployeeDetails. You must declare those methods here if you have multiple employee-related methods.
namespace SOLID_PRINCIPLES.DIP { public interface IEmployeeDataAccessLogic { Employee GetEmployeeDetails(int id); //Any Other Employee Related Method Declarations } }
Next, we need to implement the IEmployeeDataAccessLogic in EmployeeDataAccessLogic class. So, modify the EmployeeDataAccessLogic class as shown below. Here, you can see the EmployeeDataAccessLogic class implementing the IEmployeeDataAccessLogic class and providing implementations for the GetEmployeeDetails method.
namespace SOLID_PRINCIPLES.DIP { public class EmployeeDataAccessLogic : IEmployeeDataAccessLogic { public Employee GetEmployeeDetails(int id) { //In real time get the employee details from database //but here we have hard coded the employee details Employee emp = new Employee() { ID = id, Name = "Pranaya", Department = "IT", Salary = 10000 }; return emp; } } }
Next, we need to change the DataAccessFactory class. Here, we need to change the return type of the GetEmployeeDataAccessObj to IEmployeeDataAccessLogic instead of EmployeeDataAccessLogic. Internally, the method creates an instance of the EmployeeDataAccessLogic class, but we return that instance to the user using the Parent Interface, i.e., IEmployeeDataAccessLogic. This is possible because a Parent Class Reference Variable can hold the child class object reference. And here, IEmployeeDataAccessLogic is the Parent class, and EmployeeDataAccessLogic is the Child class of the IEmployeeDataAccessLogic Parent class.
namespace SOLID_PRINCIPLES.DIP { public class DataAccessFactory { public static IEmployeeDataAccessLogic GetEmployeeDataAccessObj() { return new EmployeeDataAccessLogic(); } } }
We need to change the EmployeeBusinessLogic class, which will use the IEmployeeDataAccessLogic instead of the concrete EmployeeDataAccessLogic class, as shown below. You can see the EmployeeBusinessLogic class is not using the concrete EmployeeDataAccessLogic class. Instead, it uses the nonconcrete IEmployeeDataAccessLogic class.
namespace SOLID_PRINCIPLES.DIP { public class EmployeeBusinessLogic { IEmployeeDataAccessLogic _IEmployeeDataAccessLogic; public EmployeeBusinessLogic() { _IEmployeeDataAccessLogic = DataAccessFactory.GetEmployeeDataAccessObj(); } public Employee GetEmployeeDetails(int id) { return _IEmployeeDataAccessLogic.GetEmployeeDetails(id); } } }
That’s it. We have implemented the Dependency Inversion Principle in our example using C# language where the High-Level module (EmployeeBusinessLogic) and Low-Level module (EmployeeDataAccessLogic) depend on abstraction (IEmployeeDataAccessLogic). Also, abstraction (IEmployeeDataAccessLogic) does not depend on details (EmployeeDataAccessLogic), but details depend on abstraction.
Now, you can test whether the application code is working as expected or not by modifying the Main method of the Program class as follows. Here, we are simply creating an instance of the EmployeeBusinessLogic class, calling the GetEmployeeDetails method, and then printing the employee details on the Console window.
using System; namespace SOLID_PRINCIPLES.DIP { public class Program { static void Main(string[] args) { EmployeeBusinessLogic employeeBusinessLogic = new EmployeeBusinessLogic(); Employee emp = employeeBusinessLogic.GetEmployeeDetails(1001); Console.WriteLine($"ID: {emp.ID}, Name: {emp.Name}, Department: {emp.Department}, Salary: {emp.Salary}"); Console.ReadKey(); } } }
When you run the above code, you will get the following output.
The EmployeeBusinessLogic and EmployeeDataAccessLogic classes are loosely coupled because EmployeeBusinessLogic does not depend on the concrete EmployeeDataAccessLogic class. Instead, it includes a reference to the IEmployeeDataAccessLogic interface. So now, we can easily use another class that implements IEmployeeDataAccessLogic with a different implementation, and for that, we don’t need to make any changes to the EmployeeBusinessLogic class.
Advantages and Disadvantages of Dependency Inversion Principle in C#
The Dependency Inversion Principle (DIP) is one of the SOLID principles of object-oriented design. It emphasizes that high-level modules should not depend on low-level modules, but both should depend on abstractions. Here are the advantages and disadvantages of following the Dependency Inversion Principle in C#:
Advantages:
- Loose Coupling: Classes become loosely coupled by depending on abstractions (interfaces or abstract classes) rather than concrete implementations. This makes it easier to replace implementations without affecting higher-level modules.
- Flexibility and Extensibility: The DIP allows you to introduce new implementations without modifying existing high-level modules. This makes the system more adaptable to change and new requirements.
- Testability: When high-level modules depend on abstractions, you can easily substitute mock implementations during unit testing. This enables isolated testing and promotes better test coverage.
- Parallel Development: Teams can work on different modules independently as long as they adhere to the defined interfaces. Changes to one module are less likely to impact others, reducing coordination overhead.
- Isolation of Concerns: The DIP encourages a clear separation of responsibilities between modules. High-level modules focus on the application’s business logic, while low-level modules handle implementation details.
- Plug and Play Architecture: The principle facilitates the creation of a plug-and-play architecture where new components or implementations can be integrated seamlessly.
Disadvantages:
- Design Overhead: Adhering to the DIP might introduce additional abstraction layers and interfaces, which could lead to more upfront design effort.
- Learning Curve: Developers need to understand and implement interfaces and abstractions effectively. This can sometimes require additional learning and understanding of design patterns.
- Complexity in Simple Cases: In simple applications or modules, strict adherence to the DIP might introduce unnecessary complexity.
- Performance Overhead: Depending on abstractions might introduce some performance overhead due to indirection and runtime binding.
- Over-Abstracting: Over-abstracting can lead to excessive interfaces and abstractions, making the codebase hard to navigate and understand.
- Balancing Abstraction: Striking the right balance between abstraction and practicality can be challenging. Overly abstract designs might not be appropriate for all situations.
The Dependency Inversion Principle offers significant advantages in terms of loose coupling, flexibility, and testability. However, it can introduce design complexity and requires careful consideration of the trade-offs between abstraction and practicality. Applying the DIP effectively involves finding the right level of abstraction for your application’s needs.
How to use Dependency Inversion Principle in C#?
Using the Dependency Inversion Principle (DIP) in C# involves designing your classes and modules to promote loose coupling and dependency inversion. Here’s a step-by-step guide on how to use the Dependency Inversion Principle effectively in C#:
- Identify High-Level and Low-Level Modules: Identify your application’s high-level modules (those containing business logic) and low-level modules (those containing implementation details).
- Define Abstractions: Create interfaces or abstract classes that define the contract that high-level modules will depend on. These abstractions represent the behavior that the high-level modules need.
- Implement Abstractions: Implement the interfaces or abstract classes in the low-level modules. These implementations provide the concrete details necessary to fulfill the contracts defined by the abstractions.
- Invert Dependencies: Ensure that high-level modules depend on the abstractions (interfaces or abstract classes), rather than depending directly on low-level module implementations.
- Use Dependency Injection: Apply dependency injection to provide instances of the implementations to the high-level modules. This allows you to control the behavior of the high-level modules without modifying their code.
- IoC Containers (Optional): Consider using an Inversion of Control (IoC) container to manage the creation and injection of dependencies. IoC containers simplify the process of injecting dependencies and managing object lifetimes.
- Configure Dependencies: Configure your application to inject the appropriate implementations into the high-level modules. This can be done through constructors, properties, or methods.
- Isolate Unit Testing: During unit testing, mock or stub implementations of the abstractions are used to isolate the high-level modules from the actual implementations.
- Refactor as Needed: Continuously evaluate your design and refactor as necessary to maintain clean, well-structured code. Adjust abstractions and implementations as your application evolves.
Here’s a simple example demonstrating the use of the Dependency Inversion Principle in C#. In the below example, the OrderProcessor high-level module depends on the ILogger abstraction rather than a concrete logger implementation. Dependency injection is used to inject the appropriate logger implementation (such as FileLogger) into the OrderProcessor. This adheres to the Dependency Inversion Principle by inverting the dependencies and promoting loose coupling.
namespace SOLID_PRINCIPLES.DIP { // Abstraction public interface ILogger { void Log(string message); } // Low-level module public class FileLogger : ILogger { public void Log(string message) { // Log message to a file } } // High-level module public class OrderProcessor { private readonly ILogger _logger; public OrderProcessor(ILogger logger) { _logger = logger; } public void ProcessOrder(Order order) { // Process order logic _logger.Log("Order processed: " + order.Id); } } public class Order { public int Id { get; set; } public string Name { get; set; } } }
By following these steps and designing your modules to depend on abstractions, you create a flexible and maintainable codebase that is adaptable to change and promotes good design practices.
In this article, I try to explain the Dependency Inversion Principle in C# with Examples. I hope this article will help you with your needs. I would like to have your feedback. Please post your feedback, question, or comments about this Dependency Inversion Principle article. With this, we have completed SOLID Design Principles using C# Langauge. Please give your valuable feedback in the comment section. Next, you should learn Design Patterns using C#.
Very good example and well explained. Thank you.
Excellent !! well explained..
Thanks
Well done. Thank you for sharing.
Do you have a TDD course?
Thank you.
Not now. But in coming days, we will include TDD.
Awsome Article i ever read before each and every concept clearly mentioned …!!
crystal clear explanation.. thanks much.
Awesome,Excellent brother for this example I am very confident keep sharing ..
Much Thanks
Very good example and well explained. Thank you.
Very good example and well explained. Thank you.
Very good examples of each Principles. Thank you so much 🙂
Thank you. A very good example and explanation.
Is constructor dependency Injection and DIP are same ?
Thank You Sir, for giving wonderful article.
Thank you so much for providing very good article with examples and detail explanation
Really informative, clear, and simple. Thanks for the effort!!!!
very very good examples and explanations. what a wonderful tutorial. thank you very much
The article about ‘Dependency Inversion Principle’ is simple and easy to understand.
Thank you for your valuable feeback.
your examples are really great. thank you.
Awesome, This explanation is very simple and clear,
Thank you very much.
Really awesome and simpler to understand
I am very happy for finding this great site. I couldn’t skip writing a comment to thank you, keep the great work up!
We are happy that you find our website useful. Your feedback is a lot of means to us.
Thank You so much was struggling with this concept from a long time
wooooooooo, nice article bro
Very clear explanation along with simple examples.
Thank you.
Very Easy to understand with simple and clear examples.
Thanks
Nice Articles . Simple to understand and learn.
Thanks.
very nice…would be good if you could include scenario based question series and its implementation in code
Thanks for your Suggestions. Sure we will do that in the coming days.
I have referred many different sources to understand SOLID principals along with this yours but I found this discussion as the best one. Simple to understand ,very clear with nice examples. Thank you so much.
Could you please share a link for design patterns as well , singleton, factory method?
Thank you again.
Please check our Design Patterns Articles
https://dotnettutorials.net/course/dot-net-design-patterns/
Does returning the concrete Employee in IEmployeesDataAccess create the same problem as when we return the EmployeeDataAccess in DataAccessFactory?
Good article
Sis is a veri guud artikel
You have explained each concepts of solid principle very clearly. Thank you so much.