Today, I’ll discuss principle #5 of SOLID principles to help write clean code in Python.
If you missed the other principles, here are the first , second , third , and the fourth one.
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:
<img src=“https://drive.google.com/uc?export=view&id=1IUFr50W7dcvAjNu5-QlI6GSeUNHY5Bnu”,alt=“UserManager class with a change_password() method. This class depends on EmailNotifier which is a class with a notify() method.",width=“50%">
High-level class depends on a low-level class (Designed by Plantuml)
In the UML diagram above, a dotted line joining UserManager
and EmailNotifier
with an arrowhead pointing toward EmailNotifier
indicates that UserManager
is dependent on EmailNotifier
.
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:
def change_password(username: str, oldpwd: str, newpwd: str):
pass
notifier = EmailNotifier()
# change password here
notifier.notify("Password was changed on " + DateTime.Now)
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 EmailNotifier
.
Every time EmailNotifier
changes, 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.
<img src=“https://drive.google.com/uc?export=view&id=1N5JbzkSE6cHgr5V55BoqNkUiJ0Sseyvv”,alt=“One interface: INotifier with notify() method, and UserManager class with change_password() method and a notifier in the constructor. Three classes are inheriting from INotifier interface.",width=“50%">
Design conforming to DIP. (Designed by Plantuml)
The 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.
The change_password()
method then uses this instance to call the notify()
method. If you decide to switch from EmailNotifier
to SMSNotifier
or 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).
Final thoughts:
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 EmailNotifier
, SMSNotifier
, and PopupNotifier
.
By doing so, we have made the high-level classes depend on low-level classes.
Credit
- Beginning SOLID Principles and Design Patterns for ASP.NET Developers by Bipin Joshi