This article is part of a series on the SOLID design principles. You can start here or jump around using the links below!
S – Single Responsibility
O – Open/Closed Principle
L – Liskov Substitution Principle
I – Interface Segregation Principle
D – Dependency Inversion
OK, let’s just get the Wikipedia definitions out of the way to begin with :
- High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
While this may sound complicated, it’s actually much easier when you are working in the code. Infact it’s probably something you already do in (Especially in .NET Core) but you don’t even think about.
In simple terms, imagine we have a service that calls into a “repository”. Generally speaking, that repository would have an interface and (especially in .NET Core) we would use dependency injection to inject that repository into the service, however the service is still working off the interface (an abstraction), instead of the concrete implementation. On top of this, the service itself doesn’t know anything about how the repository actually gets the data. The repository could be calling a SQL Server, Azure Table Storage, a file on disk etc – but to the service, it really doesn’t matter. It just knows that it can depend on the abstraction to get the data it needs and now worry about any dependencies the repository may have.
Dependency Inversion vs Dependency Injection
Very commonly, people mix up dependency injection and dependency inversion. I know many years ago in an interview I was asked in an interview to list out SOLID and I said Dependency Injection as the “D”.
Broadly speaking, Dependency Injection is a way to achieve Dependency Inversion. Like a tool to achieve the principle. While Dependency Inversion is simply stating that you should depend on abstractions, and that higher level modules should not worry about dependencies of the lower level modules, Dependency Injection is a way to achieve that by being able to inject dependencies.
Another way to think about it would be that you could achieve Dependency Inversion by simply making liberal use of the Factory Pattern to create lower level modules (Thus abstracting away any dependencies they have), and have them return interfaces (Thus the higher level module depends on abstractions and doesn’t even know what the concrete class behind the interface is).
Even using a Service Locator pattern – which arguably some might say is dependency injection, could be classed as dependency inversion because you aren’t worrying about how the lower level modules are created, you just call this service locator thingie and magically you get a nice abstraction.
Dependency Inversion In Practice
Let’s take a look at an example that doesn’t really exhibit good Dependency Inversion behaviour.
class PersonRepository { private readonly string _connectionString; private readonly int _connectionTimeout; public PersonRepository(string connectionString, int connectionTimeout) { _connectionString = connectionString; _connectionTimeout = connectionTimeout; } public void ConnectToDatabase() { //Go away and make a connection to the database. } public void GetAllPeople() { //Use the database connection and then return people. } } class MyService { private readonly PersonRepository _personRepository; public MyService() { _personRepository = new PersonRepository("myConnectionString", 123); } }
The problem with this code is that MyService very heavily relies on concrete implementation details of the PersonRepository. For example it’s required to pass it a connection string (So we are leaking out that it’s a SQL Server), and a connection timeout. The PersonRepository itself allows a “ConnectToDatabase” method which by itself is not terribly bad, but if it’s required to call “ConnectToDatabase” before we can actually call the “GetAllPeople” method, then again we haven’t managed to abstract away the implementation detail of this specifically being a SQL Server repository.
So let’s do some simple things to clean it up :
interface IPersonRepository { void GetAllPeople(); } class PersonRepository : IPersonRepository { private readonly string _connectionString; private readonly int _connectionTimeout; public PersonRepository(string connectionString, int connectionTimeout) { _connectionString = connectionString; _connectionTimeout = connectionTimeout; } private void connectToDatabase() { //Go away and make a connection to the database. } public void GetAllPeople() { connectToDatabase(); //Use the database connection and then return people. } } class MyService { private readonly IPersonRepository _personRepository; public MyService(IPersonRepository personRepository) { _personRepository = personRepository; } }
Very simple. I’ve created an interface called IPersonRepository that shields away implementation details. I’ve taken that repository and (not shown) used dependency injection to inject it into my service. This way my service doesn’t need to worry about connection strings or other constructor requirements. I also removed the “ConnectToDatabase” method from being public, the reason being my service shouldn’t worry about pre-requisites to get data. All it needs to know is that it calls “GetAllPeople” and it gets people.
Switching To A Factory Pattern
While writing this post, I realized that saying “Yeah so I use this magic thing called Dependency Injection” and it works isn’t that helpful. So let’s quickly write up a factory instead.
class PersonRepositoryFactory { private string connectionString = "";//Get from app settings or similar. private int connectionTimeout = 123; public IPersonRepository Create() { return new PersonRepository(connectionString, connectionTimeout); } } class MyService { private readonly IPersonRepository _personRepository; public MyService() { _personRepository = new PersonRepositoryFactory().Create(); } }
Obviously not as nice as using Dependency Injection, and there are a few different ways to cut this, but the main points of dependency inversion are still there. Notice that still, our service does not have to worry about implementation details like connection strings and it still depends on the interface abstraction.
What’s Next
That’s it! You’ve made it to the end of the series on SOLID principles in C#/.NET. Now go out and nail that job interview!
I enjoyed your take on SOLID, however [L] is really convoluted and hard to grasp, can you rework it with real-life example or something like that, I have understood the main idea but looking elsewhere, which I tried to then confirm with your take on [L] and it is still not super clear.. will read again in few days 🙂 Cheers and keep up the good work.