The Importance of Inversion of Control and the way to Achieve it using Dependency Injection

Ariel Kohan
14 min readDec 8, 2019

--

Photo by Patryk Grądys on Unsplash

In my previous article, I presented the motivation to pursue the role of a software architect and mentioned some major concepts that anyone interested in this topic should learn.

Today, we are going to discuss a principle that made one of the most significant transformations in the way I design software: the Inversion of Control, which is one of the five principles elaborated by Robert C. Martin (also known as “Uncle Bob”) in the first decade of this millennium. The rest of them are the principles of Single responsibility, Open-closed, Liskov Substitution, and Interface Segregation.

These postulates are usually referenced by the acronym SOLID and, by augmenting the flexibility and extensibility of our creations, they represent a big contribution to the way we build software nowadays.

Defining Inversion of Control Principle.

First, let’s go back to the basics of the Object-Oriented Paradigm, whose main purpose is to solve problems by creating objects that symbolize things of a delimited part of the real world called domain. The classes provide the structure of those objects, specifying fields that express their current state and exposing methods that transform that internal state or give information about it.

Objects are constantly communicated with each other by messages that trigger methods in the counterpart. Think about an object “P” of the class Person that contains an object “C” of the class Car in its fields. Assuming that C allows the necessary operations, P could call methods like startEngine, accelerate and brake. Now, let’s describe two kinds of relations between the classes.

On the one hand, we can talk about a dependency relation: the source code of the Person class depends on the source code of the Car class; in the absence of the latter, there would be a compilation error in the first one — depending on the language. We can easily find dependency relations by looking at classes import/require statements.

On the other hand, there is a relationship of control in the same course as the previous one. The source code of the Person controls what the Car object does. P decides when C starts the engine by calling that method at a certain moment or when certain conditions are met.

These two connections sharing the same direction are the simplest way to think and design objects. However, the principle of Inversion of Control proposes that, thinking with a modular perspective, we can achieve a more flexible, testable and loosely-coupled software by opposing the directions of the dependency and the control.

Later we are going to discuss this further but for the moment, let’s keep this concept like that.

It is time to put on your seat belts, we are arriving at a case study.

Case Study: My Online Store Shipments

The simplest example would be to talk about database connections and the persistence world, but who likes obvious examples? I don’t. So today we are going to talk about the world of shipping.

Imagine you are starting a new online shop selling the most amazing whatever you want. You’ll have different modules like Catalog, Order Management, Customer Support, Shipping, and many others.

We are going to focus on Shipping for this demonstration and I’ll do my best to display different stages of the project and the real value of inversion of control.

Note: the following examples are based on a multi-module Java Project. However, the code was created to be readable to anyone with OO code experience. Besides, the idea is to focus on how the different kinds of shipping modules work and how they are used. Because of that, we will just use the abstraction Application or Client to represent a module that depends on shipping. The repository is hosted on Github and contains different branches that represent the different stages of the system.

The shipping world before Inversion of Control and Dependency Injection

If we are starting a business we probably lack enough infrastructure to handle the shipping on our own. The idea here is to outsource this part of the process: we need to integrate our system with a 3rd party company to automate the shipping requests.

The Terrifying Worst Case 👻

Assume we use UPS and, to prevent verbosity in the example, we also have an SDK to integrate with them.

Hurry! We have to deliver code! What do we do? Oh, look at that UPS library, isn’t that cute? Let’s use it where we need it 😇 — ladies and gentlemen, this is the place where the terror and the most horrible creatures are hidden.

What happens next? The ShippingClient class is born — some code was deleted to reduce the size of the code. Take a quick look, especially to the shipStuff method.

Three of the four lines of the shipStuff are UPS-specific. We could add two more lines if we take a look at the private methods. That doesn’t feel quite right. What would happen if we have to ship stuff from five different modules? UPS lines would be invading those modules. What if we want to migrate to FedEx? Well… I wouldn’t like to be in front of the developers when they are told to make that modification.

You may note that we are depending on com.arielkohan.[…].UpsLibrary. That class is contained in the ups-shipping-library module, which was created to simulate a real SDK but the idea is to think about it as something managed by a 3rd party.

If we climb to the upper floor, this would be a global perspective.

Modules Diagram generated by IntelliJ Idea.

Like I said before, Application represents a module that needs the ability to ship something but we can also think that there are multiple modules with that requirement.

You can access the complete version of this stage here.

Ok, guys, it seems that we have some problems with this solution. We must refactor this and make something better. Who is with me!?

First steps in the right direction.

We reflect on that for two long minutes, we take a quick look at the code and we make the decision: send the troop to encapsulate the methods of the library.

We discover that UPS let us calculate prices and ship orders with two different priorities: STANDARD and FASTEST shipments. We have to deploy the new version before the end of the week so we rapidly create a shipping module with a Shipping class, a new class with the methods calculateStandardPrice, standardShip, calculateFastestPrice, and fastestShip. All of them receive objects from our model so we feel the satisfaction that now every module can access this functionality without UPS lines.

So now, our ShippingClient is also transformed.

The global perspective can be diagramed like this:

Diagram generated with Intellij Idea + some manual modifications.

The application client now depends directly on a module that we own, but the UPS library is not gone, it’s just an internal dependency of our shipping module.

Well, in fact, Application still has access to the UPS classes — if we depend directly on a module, we have access to their dependencies too. And it means that the previous ShippingClient would still be working. In a real-world scenario, with other lazy or ignorant devs, that could lead to some modules upgrading their dependencies but leaving some code with references to 3rd party code. That could cause trouble, couldn’t it?

Furthermore, don’t you think that this encapsulation is too much based on the external library? Same question as before: what would happen if we migrate to another provider? Probably the changes will be easier now, but we may not have the FASTEST shipment available and we would have to change our implementation. The result: we would still have to modify every module client.

In case you are not easy to convince, let me give you more. Testing. What about it? No system can compose an acceptable architecture without a good amount of testing behind it. No system can be flexible and maintainable without it. Blame on you if you can sleep lacking properly testing, at least, the most important parts of your business process.

See, how could we test the shipping client without generating real UPS orders? How can we provide a stub implementation so we can unit test our code easily without side effects? How can we simulate failure behavior?

In the following sections, we are going to refactor the code using Dependency Injection to achieve Inversion of Control. That will allow us to answer the questions and solve our problems.

Find the complete version of this intermediate stage here.

Dependency Injection For The Win

I spent a few minutes trying to find an interesting definition of dependency injection and I ran into this StackOverflow question:

How to explain dependency injection to a 5-year-old?

The question was closed because it doesn’t fit the site’s standards but, before that, there was enough time for a guy called John Munsch to make a lovely answer that we can work with:

When you go and get things out of the refrigerator for yourself, you can cause problems. You might leave the door open, you might get something Mommy or Daddy doesn’t want you to have. You might even be looking for something we don’t even have or which has expired.

What you should be doing is stating a need, “I need something to drink with lunch,” and then we will make sure you have something when you sit down to eat.

Now, allow me to translate that answer. The first paragraph is talking about the problems of not applying dependency injection: let’s play the role of a software component for a moment.

As a component, we would need the help of another component to achieve a certain task like the kid that needs a juice or something to drink. Then, we could construct the other component with all the implementation details that it needs, similar to a kid going to the refrigerator and searching specifically for an orange juice.

In that case we would be making a potential mistake: as the second paragraph of the answer suggests, we only need to achieve a certain task (drink something) and, as long as the other component allow that, we shouldn’t be worried about the other component specifics (if there is not juice, the kid could drink a soda or water and he will be fine; but if the only option that he would accept is orange juice, then he will not be able to drink). Dependency injection is the mechanism that helps us provide already-constructed specific implementations where certain characteristics are expected.

If we go back to our shipping example, we could say that the shipping clients don’t really need to know that UPS is handling the shipments for them, they just need to be able to ship stuff — even if we decide to handle the shipments ourselves, those components don’t need to and shouldn’t know it. Moreover, they don’t even need to know how to construct the Shipping implementation. Give them a well-defined interface, inject the real implementations where it’s required and you’ll get something much flexible and maintainable.

In the first sections, I mentioned that inversion of control is about having dependencies relations pointing to one direction and control to the other. Next, we will refactor our project to achieve this.

Injecting dependencies in our Project

Quick paragraph about the tools to use…

It is not the scope of this article to explain how to implement a dependency injection mechanism so we are going to use Spring Boot. What is important to mention about it is the concept of bean: objects that can be injected where we need them. These objects usually depend on other beans; Spring lets you decide if you want it to take care of all of this automatically or if you want to wrap certain objects manually. There are different ways to configure beans here but we’ll only show the ones that I used the most:

  • Special annotations (like ‘@Component’) in the implementation class of the bean. See the new version of the Shipping Client.
  • @Bean’ methods that return objects of the type of the bean you need. See the Bean method in the new Application class.

Ok, back to our project…

This time we are going to implement the best solution we can. But what that would be exactly? If we recap our previous problems, we could mention the following desires:

  1. Forbid client-modules to depend on specific shipping implementations.
  2. Client modules shouldn’t need to know the details of constructing the shipping implementation.
  3. Maximum flexibility in the architecture: there should be a way to use different shipping implementations depending on external things like environment properties or making a difference in whole-system executions versus unit tests.
  4. Client modules should be easy to test and don’t generate side effects.

The first thing we are going to do to achieve this is to create a well-defined interface. Not only a Java interface, but this will also be a whole new module with multiple files:

  • Request objects: data transfer objects or enums that we are going to receive as parameters describing the details of the request.
  • Exceptions: business errors that we could throw if something unexpected happens or if we can’t accomplish what the client needs.
  • Response objects: for simplicity, we won’t create/use them in this example.
  • The java interface itself, that will be replaced with some specific implementation.

It’s crucial to design this interface thinking in terms of our business needs, and not with the 3rd party code in mind — we can worry about that when we create an implementation that works as an adapter between our core and the library or external API that we use.

The files for the shipping-interface module:

We would also have our ups-shipping module that will contain the UpsShippingImpl implementation class:

Another important but small modification: the shipping client modules must only reference the shipping-interface module, but the module that starts the whole application should have references to all the interface and implementations so it’s able to inject real stuff where it’s needed. Because of that, we will split the previous client module between an application module and a shipping-client module. In a more realistic scenario, we would have multiple shipping clients and all of them would only have reference to the interface.

That said, let’s take a look at the files of those two modules.

The new shipping client:

The component annotation in line 12 tells spring to mark this class as a bean. At execution time, the framework will try to construct this object, considering the requirements — constructor parameters — and passing the available implementations to satisfy them.

The new Application class:

The SpringBootApplication annotation makes Spring start looking for other configurations across the whole project and mark this class as a configuration class itself, allowing us to add the bean method at line 18, where we set a UpsShippingImpl object as the implementation to use instead of the Shipping interface.

At lines 13 and 14 we start the Spring application and we ask for the shipping client bean — the one that was automatically configured with the component annotation.

Take the elevator and let’s see the global picture from above.

From the second floor we can see a simplified class diagram:

Diagram generated with Intellij Idea + some manual modifications.

If we define our Business Core as the area of our application that contains the business logic that our systems needs in order to work and Implementation Details as some specific replaceable solutions that provide some functionality, we could say that the shipping-client and the shipping-interface modules can be part of the first one and the UPS implementation, part of the latest. Remember: shipping-client could be replaced by more realistic modules like order or customer-support.

Now, take a look at the arrows of the diagram: all of them represent dependencies. Do you see how all the arrows from the UPS implementation point towards the business core? However, how do you think that control arrows would be represented? The control would be directed from the business core to the implementation detail.

In his book Clean Architecture, Uncle Bob defines this as an architectural boundary, where all the dependencies cross the boundary in one direction: from details to higher-level policies. And this, of course, can be extended to other details like the persistence mechanism or the presentation layer. The databases don’t need to be the core of our systems — in fact, they shouldn’t. With this principle and techniques, we could start our projects working with an in-memory repository and then, when we have a stronger understanding of the project, decide which kind of persistence are we going to use.

The main purpose of these actions is to isolate our business rules — our real value — from changes caused by replaceable details. Actually, now we could plug-out and plug-in new shipping implementations in a few minutes and our core won’t notice it.

Inversion of control achieved, my friends.

Let’s go to the third floor to see that more clearly.

Diagram generated with Intellij Idea + some manual modifications.

The status of our previous desires?

  • Forbid client-modules to depend on specific shipping implementations. The shipping client can’t access any UPS specific class.
  • Client modules shouldn’t need to know the details of constructing the shipping implementation. They don’t. Shipping client doesn’t know anything about how we create a Shipping object.
  • Maximum flexibility in the architecture: there should be a way to use different shipping implementations depending on external things like environment properties or making a difference in whole-system executions versus unit tests. For external properties, we could just create another implementation module and modify the Bean method to create objects depending on that value. For the scopes of tests, we could create a stubborn implementation module and add that dependency in the modules with test scope — for Java see Maven Dependency Scopes.
  • Client modules should be easy to test and don’t generate side effects. If you don’t like the option in the previous bullet, you could just create an anonymous class implementation for the test.

In the following JUnit test example, I demonstrate the case mentioned in the last sentence.

Pretty cool, right? It seems that we reached our goal.

The complete version here.

That’s it.

If we keep this mentality when we design and implement our systems, I’m sure that the quality and flexibility of our systems would be incredible.

Take a look at this tweet that Vaughn Vernon, one of the most important people in the world of Domain-Driven Design, shared a few days ago:

I don’t know what you think but I’d say that he is talking about modules with well-defined interfaces and the possibilities that this gives us.

Bonus Track For Java Devs

If you are a Java Dev that use to work with modules or if you can take a few days to learn that. There are a few tasks that you could try and will probably help you to understand everything better:

  1. Clone or fork the repository.
  2. Create another shipping implementation module and adjust the Bean method in Application to use your new implementation.
  3. Create a stub implementation for tests and adjust the maven file to import it with Test scope in the shipping client. Update the test to use that implementation.
  4. Create a new implementation module that depending on some characteristics of the request, decide if it uses a UPS implementation or the implementation that you created for (1). Hint: that module will have references to the Shipping interface, the UPS implementation and the implementation that you created.
  5. Make sure to let me know how it goes!

Contact.

If you need some help with the previous steps, feel free to add a question in comments or to send me an email to arielkohan94@gmail.com with the subject ‘IoC article’.

--

--

Ariel Kohan

Full-stack Software Engineer 🤓 Proudly accompanied by stuttering since day one 💪 Current Goal: make the world hear my voice and ideas 🎙