How to Write Clean Code (in Python) With SOLID Principles | Principle 5

Created on Feb 7, 2022

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