Today, I'll discuss principle #5 of SOLID principles to help write clean code in Python.
Let's dive into the fifth design principle; Dependency Inversion Principle (or DIP) (the 'D' in SOLID).
Note: This series of articles are to demonstrate applying the SOLID principles in Python from a learning perspective. This does not mean that you should follow this approach in production code because Python has simpler capabilities that could be used. I'll try to extrapolate on that later. For now, I'll just show you how to apply the principles in Python.
This tutorial is about the 5th design principle in a real example and shown in a UML diagram for better illustration.
So what is DIP?
Usually, you instantiate the dependencies of a class inside the class itself. The class becomes tightly coupled with its dependencies. So if you want to change the dependency, that class would need to be /changed as well. To loosen this coupling, we supply the dependencies (to a class) from the external world. That's where we can see the Dependency Inversion Principle in action.
Definition: The DIP can be stated as follows:
A. High-level classes should not depend on low-level classes. Both should depend on abstractions. B. Abstractions should not depend on details. Details should depend on abstractions.
It consists of two parts. The first part talks about the nature of dependencies between high-level and low-level classes. What are a high-level class and a low-level class in the first place?
A high-level class is a class that does something significant in the application while a low-level class is a class that does something auxiliary.
Example: Authentication and Membership System
Let's take an example. Suppose you are building an authentication and membership system for a web application that needs to manage users. As a part of user management, a way of changing the password is required. When a user wants to change the password, a notification is to be sent to the user's email about the change. In this case, the class doing the user management is the high-level class, and the class sending notifications is a low-level class.
The first part of the DIP says that high-level classes should not depend on low-level classes and both of them should depend on abstractions. Usually, a high-level class makes use of a low-level class by creating one or more instances of it within itself. Consider the classes shown below:
In the UML diagram above, a dotted line joining
EmailNotifier with an arrowhead pointing toward
EmailNotifier indicates that
UserManager is dependent on
As shown in the figure, there is a high-level class,
UserManager, that contains the
change_password() method. The
UserManager class depends on the
EmailNotifier class for sending email notifications to the user. In this case,
UserManager creates an instance of
EmailNotifier, as shown in the following pseudo-code:
As you can see, the
change_password() method instantiates
EmailNotifier and then calls its
notify() method to send an email notification.
What’s the problem with this design? After all, we have been using this style of coding for a long time. The problem here is that
UserManager has too much dependency on
UserManager might need some correction or adjustment. Further,
EmailNotifier must be made available at the time of writing and testing
UserManager. So, you are forced to finish writing low-level classes before you code high-level classes.
Additionally, future alterations to the notification system may require modifying the
UserManager class. For example, instead of email notification, you may decide to provide SMS notifications, in which case you must change the code of
UserManager to replace
EmailNotifier with the new notification class.
UserManager class no longer uses
EmailNotifier directly. Instead, an interface;
INotifier, has been introduced. The
INotifier interface is implemented by the
EmailNotifier class. The constructor of
UserManager receives an instance of a class that implements
INotifier from the external world.
change_password() method then uses this instance to call the
notify() method. If you decide to switch from
PopupNotifier, this decision won’t have any impact on the
UserManager class, as you are supplying the dependency from outside. Thus, the direction of dependencies is reversed after applying DIP.
The second part of DIP tells us that abstractions should not depend on details; rather, details should depend on abstractions. This means that you should design the
INotifier interface (abstraction) by looking at the needs of the
UserManager class. The
INotifier interface should not be designed while looking at the needs of the
EmailNotifier class (details).
The Dependency Inversion Principle is applied when you have a class that has a dependency on another class. In our example, the
UserManager class had a dependency on the
EmailNotify class. To solve that, we introduced an interface
INotifier and instantiated an instance of it in the
UserManager class constructor. By looking at the needs of the
UserManager class, we designed the
INotifier interface which was implemented by
By doing so, we have made the high-level classes depend on low-level classes.
- Beginning SOLID Principles and Design Patterns for ASP.NET Developers by Bipin Joshi
Articles in this series: