Refactoring: TF is Dependency Inversion?
If you’re touchy about language, TF stands for “The Flip” 😋, and when you read this, you’ll see why.
In a codebase, everything exists in hierarchies, whether it’s file-system or logical hierarchies. Everything depends on something else, or is depended on, else it becomes dead code. These hierarchies tend to create strong structures that can be resistant to change.
Imagine having to replace the above block. The sheer number of things that could go wrong is terrifying.
Keeping this structure easy to change is where Dependency Inversion comes in. But what is it?
What is it?
There are many explanations of Dependency Inversion e.g. “Higher-level components should not depend on Lower-level components, instead both should depend on abstractions”.
But that creates even more questions. What are high and low-level components? Why is it called “Dependency Inversion”?
I like approaching questions from the “Why” perspective, because I believe that is where meaning can be found. So let’s go … 🚀
Why is it called Dependency Inversion?
Let’s take a look at the words:
The opposite of dependent is independent, which means “free”, “unshackled”, “untethered”, so something dependent has to be “bound”, “shackled” or “tethered” to something, right? You’d be correct.
The “bound to something” implies a one-way directionality of dependency. The Child->Parent relationship comes to mind.
This means turning something “upside down”, “on its head”, “flipping it 😋".
If we were to flip the Child->Parent relationship, we’d have:
And this is supposed to capture the idea of Dependency Inversion, that we take the traditional direction of dependency between modules, and flip it.
Or does it?
What is the Traditional Direction of Dependency?
We know that code obeys a hierarchy. Things depend on other things.
The following code shows concepts not unusual to see in codebases:
UserService where their information is gotten from, but they can interact with the visual
UserProfile, making it a high-level module.
So in the traditional direction of dependency, high-level modules like
UserProfile, depend on low-level modules, like the
UserService , and the
UserService in turn, might depend on an Http API accessed via
fetch(...) , and this creates a “coupling”, “tethering” or “shackling”.
It is now impossible for the
UserService.getCurrentUser method to change without impacting the
UserProfile component, because they are bound.
So to keep
UserProfile open to change, we need to break its bond to the
UserService. How do we do this?
🎺🎺🎺 Dependency Inversion …
We ask the
UserProfile, “What do you need to do what you do?”, and it tells us, “I just need to get a name that I can display”.
So we listen to it, and give it just that.
By listening to
UserProfile, and giving it exactly what it needs, we can decouple it from
UserProfile tells us what it needs, is an abstraction. That’s what
getUsername: () => Promise<string> is, an abstraction of a function that receives a Promise of a string.
So instead of shackling
UserService, we shackle
UserProfile to its declared abstraction.
That’s a bit like binding one’s self to a religious ideal, instead of a particular religious denomination. For a bit of history, Christianity started out as one church, but began to split when the churches began to deviate from what the members who split, thought was the ideal. So these members weren’t bound to a particular denomination, but to the ideal of what they thought Christianity should be.
So modules should depend on their ideal requirements for themselves or an abstraction, rather than on other modules, and we can only find out what a module’s ideals should be, by “asking it”.
So let’s look at how we can use the
So we pass a function
() => userService.getCurrentUser().name as the value of
getUsername , providing a concrete value for the abstraction.
This way of building, is a bit like building with Legos. Every Lego piece has an interface, by which you can connect other pieces, right? So Lego pieces are not coupled with concrete, unlike brick and mortar buildings, so they are open to change.
So now, our codebase’s dependency diagram becomes much flatter. Changing a dependency will affect far fewer modules than before.
One goal of Software Engineering is to keep this number of things that need to change for a change to happen, down as much as possible. Like many things in Software Engineering, it’s a Sisyphean Task in that it gets harder, the more you try to achieve it, but it is a task that must be done.
So we now know that
- Modules should depend on abstractions
- Abstractions are defined via an interface, which is defined by types, meaning we need a good type-system to declare abstractions
The term, “Inversion” in “Dependency Inversion”, doesn’t really capture the concept, which is what makes it a little difficult to understand.
We are not flipping the dependency direction, making low-level modules depend on high-level modules, which is what “Inversion” would depict.
Maybe a better term will be “Dependency Decoupling”, or “Dependency Subversion”?
Let me know in the comments.