DEV Community

Cover image for Inversion of Control - Improve your abstractions
Soumava Banerjee
Soumava Banerjee

Posted on

Inversion of Control - Improve your abstractions

What problem does it solve?

The formulation of the problem is often more essential than its solution

If you have been writing code for a while, I'm sure you have written functionalities that are used in more than one place. It could be:

  • An utility function that does a certain task.
  • A React component that you have reused in multiple places.
  • A wrapper component that encapsulates some functionality.

Whatever it is, it's very likely that you've stumbled upon the following pattern of problems while you try to write good code:

  1. You create a reusable piece of functionality.
  2. Some use case arises that your reusable code does not support.
  3. You add options in your code (through arguments / props) and clump together chains of if-else statements.
  4. Consequently, the file continues to grow larger and the abstraction feels like a burden than a utility.

This whole scenario creates two major problems:

1. Unnecessary implementational overhead: All those branching if-else statements makes the component harder to test and decreases code readability. Also, each new if-else block compounds to the existing complexity of the code.

2.Complicated api overhead: With each new prop/argument arises the need for updating the documentation for end users (such as a future you). Also there are times when the specific functionality an end user requires is missing in the piece of code. In such cases, the developer might have to search an endless sea of documentation to verify if their use case is supported.

The Solution - Inversion of Control 🔥

Inversion of Control is a design principle. In simple terms, you define the abstractions that control the logic flow and let the developer provide their custom logic. So, your abstractions "does less work" while the "end user does more". This may seem counterintuitive but it's very powerful.

Enough talk, let's code!

In this exercise, we will be implementing a simple sorting function! Initial implementation would be a naive abstraction. Then we will refactor it and implement the inversion of control design principle.

const sortArray = (array) => array.sort();
Enter fullscreen mode Exit fullscreen mode

Now, this generic sort function is not enough for all the use cases as it sorts on the basis of alphabetical order only. Let's add the feature to specify the order of sort - either ascending or descending!

const sortArray = (array, order="ascending") => {
  if(order === "ascending")
    return array.sort((a,b) => a - b);
  else if(order === "descending")
    return array.sort((a,b) => b - a);
  else
   throw new Error("sorting order not valid")
}

// logs [100,2,2,1,1]
console.log(sortArray([1,2,100,2,1], order="descending"));

Enter fullscreen mode Exit fullscreen mode

And it's working. After some days, there comes another requirement: Support for Array of Objects. Sort them using their keys.

To support this new requirement, we again add a new flag, sortObject and implement our logic.

const sortArray = (array, sortObject, key, order="ascending") => {
  if(sortObject) {
    if(order === "ascending")
      return array.sort((a,b) => a[key] - b[key]);
    else if(order === "descending")
      return array.sort((a,b) => b[key] - a[key]);
    else
     throw new Error("sorting order not valid")
  } else {
    if(order === "ascending")
      return array.sort((a,b) => a - b);
    else if(order === "descending")
      return array.sort((a,b) => b - a);
    else
      throw new Error("sorting order not valid")
  }

}

//logs [{key: 2, name: "B"}, {key: 1, name: "A"}]
console.log(sortArray([{key: 1, name: "A"}, {key: 2, name: "B"}], true, "key", order="descending"));
Enter fullscreen mode Exit fullscreen mode

As you can see, each new requirement compounds upon our existing pile of code, thereby creating an "if-else hell". Requirements will continue to grow in the future. So do we have a way out ? Of Course we do!

Let's refactor based on IOC principle.

We will leave "how the sorting will take place" upto the developer. Our job is to just declare the abstraction, direct the logical flow and return the desired output. With this, the code becomes something like this:

const sortArray = (array, sortFn) => {
  if (sortFn) {
    return array.sort(sortFn);
  }
  return array.sort();
}

// logs [4,3,2,1]
console.log(sortArray([1,2,3,4], sortFn = (a,b) => b - a))

//logs [{key: 2, name: "B"}, {key: 1, name: "A"}]
console.log(sortArray([{key: 1, name: "A"}, {key: 2, name: "B"}], sortFn = (a,b) => b["key"] - a["key"]));
Enter fullscreen mode Exit fullscreen mode

And with that, you can see the difference! Our code is modular and we have delegated the responsibility of defining the sorting function to the end user. Testing this new piece of code is much more easier than before too!

Do you feel you've seen this pattern before ?

Sure you have. Many common inbuilt functions like map, filter, reduce, sort, forEach implement this design principle!

Not only that, React hooks, state-reducer pattern is based on IOC too. Many web frameworks are based on this principle. Dependency Injection, a common and important design pattern, is based on the principle of IOC as well.

Conclusion

Just like any other tool, we should understand the need to implement IOC. If we have an idea of conditions and branches in our logic, we can implement non-inverted abstractions. On the other hand, if we are dealing with unknown number of functionality extensions, inverting the control might be the ideal choice.

Extra bit!

Part 2 of this article would feature how to use Inversion of Control to implement role based protected routes using react and react-router-dom v6. Stay tuned!

Credits:

This article is heavily inspired from Kent C. Dodds 's article on the same. Here's the link to the article.

Top comments (0)