What does dependency inversion mean or look like in the context of a dynamically typed language like Python? First we’ll review the concept of dependency inversion and what it means in a statically typed language like Java. Then we’ll compare the differences in dependency inversion between the two types of languages.
Note: Robert C. Martin originally introduced this term and while I’m not certain I think it likely came out of the java world because I can’t imagine pythonistas or rubyists coming up with such a grandiose word for what is ultimately a fairly simple and specific means of indirection 🙂
Dependency Inversion
According to wikipedia, the dependency inversion principle states that:
- High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
Put another way, it’s saying that if a component needs to be able to switch specific implentations (details), it should not have a source code dependency to those implementations.
By removing the source code dependency and replacing it with a dependency to an interface, you are “inverting” the depedency between the high level component and the low level details.
But that’s all very theoretical mumbo jumbo so lets look at some concrete examples.
Java
In java, this is an example without dependency inversion.
class Cat {
public void greet() {
AngryGreeter greeter = new AngryGreeter();
greeter.greet();
}
}
Code language: JavaScript (javascript)
The Cat
class is our component here. It is directly referencing the class AngryGreeter
which contains the specific details of greeting.
In many cases, this source code dependency runs in the same direction as the flow of control. In other words, if Cat
needs to invoke methods on instances of AngryGreeter
, it needs to have a reference to a concrete greeter object and the most common way to do that is to just use the new
keyword to construct the object where it’s needed.
The consequence of this relationship through this construction using new AngryGreeter
is that it creates tight coupling between the two modules. More specifically, our Cat
class now directly depends on the concrete greeting class AngryGreeter
.
Now, if there is no need to add new greeting behaviors at all, this relationship is perfectly fine.
But if we want to add new behaviors such as a HappyGreeter
, we would have to modify Cat
. Taken to the extreme, if we needed to add 100 greeting behaviors over the course of 100 days and we wanted Cat
to be able to perform all of them, we would have to change Cat
100 times!
To avoid that, we need to:
- Introduce an interface that concrete greeters impelement
- Pass concrete greeters as arguments into
Cat
(dependency injection)
Here’s what dependency inversion looks like in Java for this simple example:
public interface Greeter{
void greet();
}
class AngryGreeter implements Greeter {
public void greet() {
System.out.println("YOWL!");
}
}
class Cat {
private final Greeter greeter;
public Cat(Greeter greeter) {
this.greeter = greeter;
}
public void greet() {
this.greeter.greet();
}
}
Code language: PHP (php)
Now, rather than Cat
depending directly on AngryGreeter
, both Cat
and Angry
greeter depend on the interface Greeter
.
What we have now is a dependency that points in the opposite direction of the control flow, hence it’s inverted.
Now, we can add new greeters to our system without ever touching the Cat
component. That’s pretty sweet. Previously, Cat
had a direct, hard coded reference to AngryGreeter
. Now it has a direct reference to the interface
instead which will only change when the greeting API changes (and not when new greeting behavior that uses the same API is added).
Python
Now lets look at the first example implemented in Python:
class AngryGreeter:
def greet(self):
print("YOWL!")
class Cat:
def greet(self):
greeter = AngryGreeter()
greeter.greet()
Since python is dynamically typed, there is no need to declare an interface the source code in the same way as Java. At run time, objects either can do something or they can’t.
Therefore, if we wanted to invert the dependencies, we can turn the direct reference to the object into a parameter:
class AngryGreeter:
def greet(self):
print("YOWL!")
class Cat:
def greet(self, greeter)
greeter.greet()
Code language: CSS (css)
While this is easy to do in python (or any dynamically typed language), there is one glaring drawback when we do this: we don’t know exactly what the interface is without looking at what methods are actually being called.
So while inverting dependencies are easy because interfaces are implicit, the implictness of interfaces means that:
Cat
may crash if given agreeter
object that does not implement the same interface. Missing method?- You need to spend a lot more time reading existing source code (mostly existing concrete classes) in order to infer what the interface is
This is one of the fundamental differences (and probably source of many divorces) between statically typed and dynamically typed languages. Given a dynamically typed language like python, can we add more reassurance?
There’s a couple of things you can do.
The first is type hints, which can be useful for your IDE but does not enforce contracts at runtime. The second is abstract base classes which will provide runtime checking.
Abstract Base Classes
Python 3 introduced ABC classes to solve these problems (these are more akin to javas abstract interfaces since they can contain implementation).
import abc
class Greeter(abc.ABC):
@abc.abstractmethod
def greet(self):
pass
class AngryGreeter(Greeter):
def greet(self):
print("YOWL!")
class HappyGreeter(Greeter):
pass
class Cat:
def greet(self, greeter):
greeter.greet()
if __name__ == "__main__":
c = Cat()
c.greet(AngryGreeter())
c.greet(HappyGreeter())
Code language: CSS (css)
Now, you’ll get an exception when HappyGreeter is instantiated without the greet method. In terms of understanding what methods are expected, we don’t have to go digging through multiple classes – we can just look at the interface that the greeters implement.
Leave a Reply