DIP, IoC, DI — Know Them Better

Elad Israeli
Nielsen-TLV-Tech-Blog
7 min readJan 25, 2021
Photo by Kevin Ku on Unsplash

We all have heard about these confusing terms “Dependency inversion principle,” “IoC,” “IoC Container” and “Dependency Injection” — So have I, and that’s exactly why we are here, today in this post, I’m finally going to explain what is their real meaning.

What are these terms’ definitions?

The first term is the Dependency Inversion Principle (DIP), and it’s one of the 5 rules that stand out for S.O.L.I.D.
It says that every entity should be dependent on abstractions and not on concretions.

A nice common example is described with Button and a Lamp :

Now Button only depends on abstractions; it can be reused with various classes that implement Switchable.
Changes in Lamp will not affect Button.

The second term is Inversion of Control (IoC), IoC means that the control of the flow is inverted and makes the programmer not responsible for who controls how and when objects are being called.

The third term is Dependency Injection(DI) is one of many implementations of IoC. The act of connecting objects with other objects, or “injecting’ objects into other objects, is done by an assembler rather than objects themselves.
To achieve this ability to inject objects, our modules should communicate over interfaces and abstractions and not directly with each other.
Different frameworks offer different ways, such as XML files, Annotations, and Decorators, to specify which concrete implementations will be injected into abstractions.

The last term is IoC Container is a framework that implements the IoC principle by implementing automatic dependency injection. It also manages object creation and its lifetime and injects the required dependencies to the intended classes.

Here we can see a graph that makes the difference between these terms more clearly.

Motivation

After we have a clear vision of each term's definition, we can define our motivations behind using these concepts.

One fundamental aspect is that we usually face the overhead of changing modules during our project's lifetime.
Since systems are made up of many components that change over time, it requires us as engineers to easily change and replace some parts of the component without destroying any existing functionality and being on time with the project’s delivery.

Sometimes when our projects are getting bigger, we face objects that become tremendously huge and contain many more dependencies than we had at the beginning of the project.

We can start with:

let svc = new ShippingService(new PricingService());

And end up with:

let svc = new ShippingService(new ProductLocator(), new PricingService(), new InventoryService(), new TrackingRepository(new ConfigProvider()), new Logger(new EmailLogger(new ConfigProvider())));

We want to eliminate it and avoid the situation where we have to handle complex dependencies.

Another great motivation is for folks who like this because TDD goes very well with IoC.

What implementation are we going to focus on?

Now that we have a clear understanding of IoC and DI, we’ll see some implementation examples.

In addition to the Dependency Injection concept, before we are diving into details, I want to emphasize another crucial subject for understanding our approaches in the next subjects.

I believe that some of you have heard about clean architecture, and if not, let’s shed light on that concept.

Clean Architecture: A Craftsman’s Guide to Software Structure and Design (Robert C. Martin Series)

The circles in the graph above represent different areas of software. The inner the circle is, the higher level the software it represents.
As Uncle Bob says:

“The outer circles are mechanics. The inner circles are policies.”

The overriding rule that makes this architecture work is The Dependency Rule:

Nothing in the inner circle can know anything at all about something in the outer circle. Nothing in the inner circle shouldn’t be mentioned in any class, function, variable, or other named entity of the outer circle.

We know that each layer/circle should communicate with the other surrounding circle. This communication should not be direct because it would violate the Dependency Rule — No name in the outer circle should be mentioned in the inner circle and vice versa. It means that interfaces or abstractions should make the communication and implementation happen in the inner circle.

We see that all the clean architecture depends on the Dependency Rule.

At Nielsen, we built an infrastructure that follows the clean architecture and IDesign concepts.

We mostly use the CQRS pattern by having Commands and Query separately implemented.

We also use interfaces and abstraction between each layer we have.

We split each module in our application to:
API where we will find the Routes and the Controller.

The second layer is the Business Logic that contains the Commands implemented by a Manager — the manager “manages” the flow by using engines or gateways that encapsulate and perform the business logic or data accessing.
The third is the Data Access Layer — Repositories abstractions and Gateway abstractions. We treat them as layers that access our data, and we mostly use abstractions here.

Let’s see an example from one of our services that run on production on a large scale and treat thousands of requests a day.

One of the definitions mentioned earlier is IoC Container, and our infrastructure was built with InversifyJS as the framework for the IoC Container.

InversifyJS is a lightweight (4KB) inversion of control (IoC) container for TypeScript and JavaScript apps. An IoC container uses a class constructor to identify and inject its dependencies.

Step 1: Declare your interfaces and abstractions

InverisfyJS documentation

Step 2: Declare your types

InverisfyJS documentation

Step 3: Declare dependencies using the @injectable & @inject decorators

InverisfyJS documentation

Step 4: Create and configure a container

InverisfyJS documentation

Step 5: Resolve dependencies

InverisfyJS documentation

Infrastructure Architecture

Defining our injectable class and the injected classes as well

In the example above, we can see that we extensively use Command and Query to represent our actions and queries we intend to do in the controller. This way, we are making sure that each layer does not directly interact with the other and know about the other layer, using an interface to communicate.

Let’s dive into one of our commands

We see here a command that uses Dependency Injection to bind between the abstractions it contains.
Let’s focus on the repository. Here we can see that the command uses a repository that lives on another layer compared to the command. The command doesn’t know anything about who implements this repository, what database is being used by the repository, or any details about how it is implemented. It’s only concerned about the interface.

By assimilating the concept of DIP, we can easily change the implementation of the Repository, and the Command does not care about it. It can be easily changed whenever it is needed.

The Command also uses the engines FlowValidator and DeliveryServicePublisher that have one responsibility, and they describe a specific business rule.

Defining our symbol types for each module

Container configurations

In this part, we define our Container to create the mapping between the Interfaces to the implementations. We use the Symbols we created in the previous step to specify the specific implementation we want to see for the generic abstraction. From now during our application, we will use these symbols to make sure our components are injected with our desired implementation to the interfaces.

Composition root configuration where we resolve the dependencies of the controller

Conclusion

We started with many terms that feel like they share the same meaning and realized their differences.

We understood the role of each definition, and now we can distinguish between them easily.

We drilled down into the motivation behind using these patterns and concepts. We saw problems and the patterns and concepts that can solve them.

We then decided to focus on the DIP concept and particularly on the DI pattern. We referred to some architecture concepts and saw how they are support and an integral part of the desired architecture we want to achieve in our application.

We then saw a live production infrastructure architecture that is used by our backend services in Nielsen.

I hope you like the post and finally can differentiate between these confusing terms!

Feel free to give me some claps and ask questions. I’m here and also on Linkedin.

--

--