- Published on
- View count
SOLID Principles, Part 5: The Dependency Inversion Principle
- Authors
- Name
- Vishwajeet Yadav
- @vishwaj33t
Don't Call Us, We'll Call You
We've arrived at the final letter of the SOLID acronym: "D," which stands for the Dependency Inversion Principle (DIP). This one can sound a bit intimidating, but it's based on a powerful, real-world idea that you might know as the "Hollywood Principle": Don't call us, we'll call you.
The Dependency Inversion Principle has two parts:
- High-level modules should not depend on low-level modules. Both should depend on abstractions (like interfaces).
- Abstractions should not depend on details. Details should depend on abstractions.
In simple terms, this means that a high-level class (like a UserService
that handles business logic) shouldn't be tightly coupled to the specific, low-level tools it uses (like a PostgreSQLDatabase
class). Instead, the high-level class should only know about an abstraction, like a generic Database
interface. The specific database class can then be "plugged in" from the outside.
This "inverts" the typical flow of control. Instead of the UserService
creating and controlling its own database connection, the control is inverted, and the database connection is given to the UserService
.
How to Invert Your Dependencies
The most common way to apply the Dependency Inversion Principle is through a technique called Dependency Injection. This is a fancy term for a simple idea: if a class needs a tool to do its job, don't let it create the tool itself. Instead, give (or "inject") the tool to the class when you create it, usually through its constructor.
To make this work, you need to rely on abstractions over concrete implementations. You'll define an interface
that describes what the tool can do (e.g., a Database
interface with a getUser
method). Your high-level class will only know about this interface. Then, you can create multiple concrete classes (like PostgreSQLDatabase
or MockDatabaseForTesting
) that all implement this same interface.
This allows you to swap out the low-level details without the high-level class ever knowing or caring.
The Good and Bad of Inverting Control
Flipping the flow of control is a powerful technique, but it comes with its own considerations.
The Power of "Pluggable" Code
The biggest benefit of DIP is that it dramatically reduces coupling between your classes. Your high-level business logic is no longer tied to the nitty-gritty details of your database or email server. This makes your system incredibly flexible. Need to switch from a PostgreSQL database to a MySQL database? No problem. Just create a new MySQLDatabase
class that implements your Database
interface and inject that instead. The UserService
doesn't need to change at all.
This flexibility makes your code much easier to test. When you're testing your UserService
, you don't need to connect to a real database. You can just create a simple MockDatabase
class that implements the Database
interface and returns fake data. This makes your tests faster, more reliable, and completely independent of external systems.
The Cost of Abstraction
The main trade-off is that DIP can introduce a bit more complexity into your project. You now have interfaces to manage and dependencies to inject, which can feel like more moving parts, especially if you're new to the concept.
There's also a small risk of the overuse of interfaces. Not every single class needs to be hidden behind an interface. This principle is most valuable for decoupling major parts of your system, like the business logic from the data access layer, or your application from external services.
Let's See an Example
Imagine a UserService
that needs to fetch user data from a database.
The Tightly Coupled Way
In this version, the UserService
knows it's using a DatabaseService
. It creates its own instance of it. This is a problem. What if we want to test UserService
without a real database? We can't. What if we want to switch to a different kind of database? We have to change the UserService
code.
// The high-level UserService depends directly on the low-level DatabaseService.
class DatabaseService {
getUser(id: string): User {
// Logic to get a user from a real database...
}
}
class UserService {
private database: DatabaseService
constructor() {
// The UserService is creating its own dependency. This is a tight coupling.
this.database = new DatabaseService()
}
getUser(id: string): User {
return this.database.getUser(id)
}
}
The Decoupled, Inverted Way
Here, we "invert the control." The UserService
no longer knows or cares what kind of database it's using. It only knows about the Database
interface. We "inject" the actual database service from the outside.
// First, we define the abstraction.
interface Database {
getUser(id: string): User
}
// Then, we create a concrete implementation.
class DatabaseService implements Database {
getUser(id: string): User {
// Logic to get a user from a real database...
}
}
// The UserService now depends only on the abstraction.
class UserService {
private database: Database
// The dependency is "injected" from the outside.
constructor(database: Database) {
this.database = database
}
getUser(id: string): User {
return this.database.getUser(id)
}
}
// Now we can create and "inject" any database we want!
const databaseService = new DatabaseService()
const userService = new UserService(databaseService)
This version is flexible, testable, and a perfect example of the Dependency Inversion Principle in action.
The SOLID Series
And with that, our journey through the five SOLID principles is complete!
- S: Single Responsibility Principle
- O: Open-Closed Principle
- L: Liskov Substitution Principle
- I: Interface Segregation Principle
- D: Dependency Inversion Principle (You are here!)