Python Dependency Inversion Principle

Python Dependency Inversion Principle

Dependency Inversion is one of important principles defined in SOLID design. Understanding this principle leads to understanding principles behind clean architecture and unit testing. In this article we will find out how important it is.

Direct dependencies

Third-party libraries

Let's imagine that we are writing push noticiations component inside our backend project. We decided to use firebase API as push gateway. To do that we've installed third-party library developed by firebase.

from firebase_admin import messaging


def send_push_notification(message: str, device_token: str) -> None:
	message = messaging.Message(
    	...,
	)

	response = messaging.send(message)

We can see that our function send_push_notification uses messaging module directly from third-party lbirary called firebase_admin . New library serves as a new abastraction for interacting with complex things like Firebase Server . Main reason of using new abstraction is to reduce number of actions required to send push notifications. In other words we save time by using third-party library.

Using new abstractions means that we depend on it. Now we depend on third-party library firebase_admin . This kind of dependency is called direct dependency

Direct Dependency. send_push function uses(depends on) firebase library, firebases library uses(depends on) firebase server API

High coupling

Direct dependencies are hard to change. Now your function is highly coupled with this firebase library API. We can say that this code is nailed to third-party library. In order to change your push gateway provider(let's say to Expo) you have to do some serious work in your code. In real project this code is pretty complex and changing to another library will take some time and pain.

Function send_push_notification is highly coupled with firebase library

Testing

Testing with direct dependencies means that your code triggers all coupled components from up to bottom. In our case send_push_notification function calls firebase library functions and classes.  There's no way to change third party dependency for testing except that you can mock third party library. It means it's very hard to implement unit tests with this code. In this test we trigger every component, so it's more integration/E2E test.

Tradeoffs

I don't like to call something good or bad. I would say this direct dependenices(high coupling) has such tradeoffs:

➕ Pros :

  • simple implementation;
  • less time to implement and understand;

➖ Cons :

  • we can't change dependencies quickly;
  • we can't write proper unit tests without mocks;

Inversed dependices

Reverse arrows

Let's do a trick. Take our first image and inverse arrows(inverse dependencies). What do you see?

Inverting dependencies

At first glance it doesn't make any sense. How third-party library can depend on our code? Should we fork the library and directly use our code to depend on it? Sounds bad.

New Abstraction

We need to recall that third-part library is an abstraction that reduces complexity of interaction with Firebase server. send_push_notification depends on this abstraction(third-party library).

We can't modify our third-party library, but we can create our own abstraction around it. We can use abstraction as our tool for inverting dependencies. We will invert our abstractions. Let's add another layer of abstraction. Custom abstraction designed by us.

Dependencies are still direct. Let's continue our process of thinking.

This time send_push creates an instance of class PushNotification which is a wrapper using third-party library.

Depending on abstract class

Now we will make our FirebasePushNotification to depend on an interface. Abstract classes in python serve as interface. You can define abstract class and some methods. Classes who inherit this abstract class must implement all the methods in parent class marked as abstract.

Let's take a look how we've done this:

Seems we have inversed dependencies for FirebasePushNotification class. Now it depends on abstract class. Sound's like we're heading in right direction.

How can we make send_push to depend on abstract class?

Simply moving push_notification as kwarg.

def send_push(push_notification_service: PushNotification, ...) -> None:
	push_notification = push_notification_service()
    push_notification.send(...)

We made our code to depend on abstraction, rather than concrete implementation. That's what we wanted.

Low coupling

So, by introducing a couple of abstractions and using interfaces we can achieve inversed dependencies. We've decoupled our components. Single interface compared to direct usage of the whole third-party library decoupled our code.  Now it's possible to change notification system backend by implementing new class without huge efforts to change other parts of our code. Argument of interface type adds interchangebility.  

Testing

Decoupling our push notification backend/gateway now allows us to test our functions with fake implementations. It means we don't need to mock the dependencies. Instead just implement FakePushNotification class and pass it to our push_notification function. Therefore with inversed dependencies we can implement unit tests.

Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an InMemoryTestDatabase is a good example). (c) Martin Fowler
Fakes

Tradeoffs

➕  Pros:

  • low coupling - interchangeable components;
  • unit tests without mocks;

➖ Cons:

  • new abstractions add complexity, code is harder to read;
  • new abstractions require effort to maintain;

Resume

Code using  third-party libraries directly has  direct dependencies. It's a simple solution, but it nails our dependencies and doesn't allow to implement unit tests without mocks.

We can invert these dependencies by introducing new abstractions via abstract classes and their implementations. Client code should have an argument of abstract type to inverse its dependencies. It allows to decouple components, change dependencies on the fly and do unit tests without mocks.