Better Programming: The Dependency Inversion Principle
Jul 30, 2023
In this series of articles you will become familiar with the SOLID principles, which will help you write more modular, understandable, and maintainable code. SOLID is an acronym that encompasses the following principles:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
In this article we will explore the last of these principles, called the Dependency Inversion Principle.
To learn Python and build the skills to land your first job, check out our Professional Python Developer Bootcamp.
How to apply the Dependency Inversion Principle
When we design an application, we will create some modules whose purpose is to provide certain functionality for other modules to use. We will call the former low-level modules and the latter high-level modules.
Continuing with our streaming app example, the REST interface for downloading music, the database for saving metadata, and the local cache mechanism for the downloaded music, are examples of low-level modules. Their goal is for another higher-level module to use them to play and manage our music library. In our example, this could be implemented in the module where we create our music player.
Well, the Dependency Inversion Principle states that in our application, high-level modules should not depend on the details of low-level modules. Rather, they should communicate with them through a generic interface, which remains stable even when the implementation details of the low-level module it replaces change.
It sounds complicated but it’s not. Let’s look at an example to understand it better. Imagine we have a MusicPlayer class that controls music playback. The user pays for either Spotify or Apple Music, and when the user selects a song, our music player has to fetch it from their streaming service.
One way to implement this logic would be as follows:
However, this implementation tightly couples the MusicPlayer class to the streaming services. That means, that if we support a new streaming service in the future, we will have to change the MusicPlayer class to include it. Imagine this a bigger scale, with hundreds of streaming services and many classes that depend on them. It is the recipe for an unmanageable app.
To solve this, we can make the MusicPlayer interact with a generic MusicService, which is an abstract interface that every concrete streaming provider will abide by. Subclasses of this generic MusicService will implement the methods it defines in order to make them compatible with the MusicPlayer.
As you can see in the previous example, now our MusicPlayer class can work with any streaming service, and it does not even care which specific provider the user has. The code inside this class has been designed to interact with a MusicService, and any streaming provider that we will implement in the future will be a subclass of this interface, therefore ensuring compatibility.
Benefits of using the Dependency Inversion Principle
Applying the dependency inversion principle provides several benefits:
Reduced coupling
Changing implementation details in lower-level modules will have minimal impact on its higher-level counterparts, so we do not have to go back and forth adjusting their inner workings to accommodate each other.
Code readability
Since the higher-level modules don't include implementation details of its lower-level counterparts, reading and understanding their functionality is much easier.
Increased flexibility
You can implement new versions of a service without altering the functioning of the program, just by making sure they follow the specified interface.
AI moves fast. We help you keep up with it.
Get a monthly selection of the most groundbreaking advances in the world of AI, top code repositories, and our best articles and tutorials.
We hate SPAM. We will never sell your information, for any reason.