Inversion of Control — A simple & effective design principle
Gaurav Behere
Posted on October 17, 2021
Reducing code complexity with IoC
Inversion of control (IoC)
If you have heard of dependency injection(DI) you have been using inversion of control but maybe not knowingly.
IoC is often seems used interchangeably with DI but IoC as a concept is much more than that.
IoC is a design principle that helps you in reducing the complexity of code that you may want to ship as a reusable component or a library.
DI is one of the patterns that help in implementing IoC.
Usually, we have seen libraries/components implementing all the features for us & expose APIs to be called in a certain way to get the functionality we need. We may call the same API with a different set of parameters & value combinations to get what we want.
There is a level of abstraction to us in a way that we need not bother about the library/component reusable code but we need to know the meaning of each option/parameter to be passed so that we can understand the API’s behavior better.
Now put yourself in the shoes of the guy who wrote that library or the reusable component.
There is n number of use cases that he needs to support out of the same piece of code. There can be different values of the parameters & different combinations of those which may result in the same API or component to behave differently.
What does this translate to in code?
Lots of IF ELSE statements
What does it lead to?
- More cyclomatic complexity
- Less maintainable code
- Lengthier documentation about all the options & their combinations
Any new feature that our generic component now has to support will have to be done very carefully so that we don’t break any existing support.
When we refactor the code it is not easy to get away with any option or any conditional branch as we may not know who is consuming our component using that code flow.
All these are very usual problems we see almost every day, isn’t it? This is an ever-growing problem too as the request for new functionalities with more if-else will keep coming.
Let’s look at this piece of code to understand the problem better.
You are writing a function that does the sorting of an array:
const sortArray = (array) => array.sort();
At a very basic level, it just returns the native sort. This is not sufficient as it doesn’t work well with numbers & custom sort for objects, also the default order of sort would be ascending. Let's add these features one by one.
Let us add support for descending sort:
// order = 1 -> ascending
// order = 2 -> descending
const sortArray = (array, order=1) => {
if(order === 1)
return array.sort();
else if(order === 2)
return array.sort((a,b) => b - a);
else
console.error("Unsupported sort order provided")
}
Let us add support for sorting objects with a specified key:
// @param order(number) = 1 -> ascending
// @param order(number) = 2 -> descending
// @param objectSort(boolean)
const sortArray = (array, objectSort, key, order=1) => {
if(objectSort) {
if(order === 1)
return array.sort((a,b) => a[key] - b[key]);
else if(order === 2)
return array.sort((a,b) => b[key] - a[key]);
else
console.error("Unsupported sort order provided")
}
else {
if(order === 1)
return array.sort();
else if(order === 2)
return array.sort((a,b) => b - a);
else
console.error("Unsupported sort order provided")
}
}
As you can see that addition of features is adding code paths & branches in our code. Now say we need to support a case insensitive sort based on an option & we want to keep all the undefined values at the start of the array, that too based on an option, how many more if-else do we need?
This list of features is ever-growing.
I took the example of sorting as a library function because the native sorting in JavaScript is also based on the principle of IoC.
Inversion of Control
As Wikipedia explains it:
A software architecture with this design inverts control as compared to traditional procedural programming: in traditional programming, the custom code that expresses the purpose of the program calls into reusable libraries to take care of generic tasks, but with inversion of control, it is the framework that calls into the custom, or task-specific, code.
In simple terms, in the inversion of control, the library or the reusable component lets the consumer take control of what the functionality is & it provides an abstraction on top of it.
Now imagine passing the sorting method as a parameter to the library & it actually invokes your own sorting method to do the sorting.
How does it help?
The extensibility of functionality is now independent of the code complexity in the library rather the consumer gets a handle to override the default behavior in its own way.
const sortArray = (array, sortFunction) => {
if (sortFunction) {
return array.sort(sortFunction);
}
return array.sort();
}
- Testability: We can substitute the core functionalities with mocks during the testing.
- Substitutability: We enable a plugin architecture that makes it easy for us to swap out plugins, and program against code that doesn’t yet exist. All we need to do to substitute the current dependency is to create a new one that adheres to the contract defined by the interface.
- Flexibility: According to the “Open Closed Principle”, a system should be open for extension but closed for modification. That means if we want to extend the system, we need only create a new plugin in order to extend the current behavior.
- Delegation: IoC is the phenomenon we observe when we delegate behavior to be implemented by someone else but provide the hooks/plugins/callbacks to do so. We design the current component to invert control to another one. Lots of web frameworks are built on this principle.
There are many real-life use cases where you would have seen IoC in action. A good example is a state reducer pattern.
React, rather than providing a complex way of managing your state, lets you do that with your own reducer function & lets you provide your reducer as a hook before rendering your components.
Dependency Injection in angular is also based on this principle. Dependency Injection (DI) is one of the implementations of IoC based on the composition of dependencies in the container (the library).
Hooks in React are based on the IoC too.
Conclusion
Though IoC is a good principle to follow & there is a large number of libraries following it, it should be a conscious decision to choose IoC. In case you are aware of all the possible functionalities & code branches, a non-inverted control would make the consumption of the library easier. If you dealing with unknown extensibilities, it would be recommended to implement an inverted control.
Posted on October 17, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.