Vikas was playing foosball with his colleague in the office. They were planning to participate in a Foosball Premier League (FPL) happening in their company Zeamazon Inc. Suddenly he saw his manager, Rahul approaching his cubicle.
‘Vikas, do you have a minute ?’ - Rahul prompted.
‘Gotta Go Guys, seems like Rahul is looking for me.’ - Vikas said reluctantly.
Rahul to Vikas - ‘Our testing team reported a bug this morning, I want you to look into it on priority, and let me know how soon can we expect the fix in production.’
Vikas went to his cubicle and looked at the bug. He immediately realized it was caused due to a recent change that he made to the module. ‘But I don’t need to tell him that’ - he thought to himself.
He knew the fix will take around a week's time, so he replied to Rahul - ‘It will take a couple of weeks to fix this’, - so that there is ample time to prepare for FPL.
One week later, Vikas fixed the bug and was about to submit his changes but something was not feeling right. ‘I don’t want to repeat the previous mistake, it will be better to check all the dependent modules as well.’
Two weeks later, Rahul came to his cubicle and inquired — ‘Vikas, you said you will be done in two weeks. Why haven’t you submitted the code and how much more time do you need?’ And Vikas hesitantly uttered the most dreaded words of the IT Industry — “I don’t know, It’s more complicated than what I thought!”
Symptoms of Bad Software Design
What happened with Vikas is an indicator of the Bad Code Quality of the software he was working on. The three most common indicators of a Bad Quality code are:
- Rigidity: Software that is hard to change because every change affects other parts of the system.
- Fragility: Changes in one module break some other irrelevant module.
- Reusability: It is hard to disentangle desirable parts of code for Reuse.
And the common theme that impacts all three major symptoms of bad design is Dependency.
Bulk of Modern Software Development is about managing dependencies.
~ Robert C Martin
Let that sink in a little bit. We as software developers are rarely solving complex algorithmic problems, most of the time we spend is on writing a modular CRUD-based application. That means wise management of dependencies is crucial for ‘Good Software Design’. How do we do that? This is where the D of S.O.L.I.D principles comes into the picture. Which states:
“Your high level policies should not depend on low level policies.”
~ Robert C Martin
This is also known as The Dependency Inversion Principle.
Any software that you have created can be visualized in levels of abstraction. Starting from your main() method as root to every function as a leaf in your codebase.
Since this ‘tree’ can be quite big and daunting to analyze, we can label these code dependencies as High and Low-level dependencies where High-level dependencies are those that directly interact with your main method or root module and Low-level dependencies are the methods that do the actual work.
Our ‘Higher-level’ module usually calls our ‘lower’ level modules in order to get some work done. This leads to all of our higher-level modules being dependent on our lower-level modules. This means that any change in your ‘low-level policy’ is bound to have an effect on your ‘high-level policy’.
The dependency inversion principle attempts to invert this relationship so that both of your higher and lower-level modules are dependent on abstraction instead, ensuring a separation of concerns between high and low-level modules.
Notice that in the diagram (2), the lower-level modules are now dependent on higher-level abstraction, which basically inverts the direction of control flow. If both client and service depend on abstraction, then you essentially have an agreement that, if respected, will allow for the following:
- Rigidity: Changes in the lower-level modules will not ‘bubble up’ to higher-level modules.
- Fragility: The client will be agnostic of who the dependency is, and instead rely on what it does. This will ensure that changes in one part of the system will not break any other system.
- Reusability: Both client and service can be reused in other contexts safely, trusting that their dependencies will respect the contract.
In Conversation with The Wise Developer
Vikas: ‘Master, what makes a code hard to read ?’
The Wise Developer: ‘Suppose you are trying to understand how an HTTP service works by reading the code, and somehow you find yourself stuck inside a function that deals with string buffers.’
A high-level module polluted with low-level details makes the code hard to read.
Your high-level modules should be declarative, i.e they should focus on what needs to be done rather than going into the details of how it's done. Violating this principle not only makes your code less readable but also exhibits all the properties of bad software design mentioned earlier.
Dependency Injection vs Dependency Inversion
Vikas: ‘What is the difference between Dependency Injection and Dependency Inversion? I have heard people using them interchangeably.’
The Wise Developer: Dependency Injection is a pattern that deals with supplying or injecting dependencies into a module so that the module can focus on actual work rather than trying to figure out how to instantiate and fetch a dependency. Whereas, Dependency Inversion is a fundamental principle that attempts to invert the relationship between client and service module to decouple them from each other.
Enjoy learning, Enjoy oops, Enjoy algorithms!