Back to the homepage
Angular

Angular & Dependency Inversion Principle

Introduction

This is the last article of the series about SOLID. It is a set of principles that allows us to write a code which is easier to scale and to change the behavior of our application, without modifying a significant amount of code.

The set of rules consists of:

Now we are going to deal with the Dependency Inversion Principle.

Dependency Inversion Principle

Just like you can see in the picture when using electrical devices, we would rather not solder them directly into the electrical system. Instead, we simply plug the device into the socket ?

So you can think of this rule as creating “sockets” in our code to which we can interchangeably connect other devices (services, functions, etc.).

Formal definition

It sounds as follows:

  • high-level modules should not depend on low-level modules
  • both should depend on abstractions
  • abstractions should not contain details (because those details should already be in the concrete implementation)

What benefits do we have by sticking to this rule?

  • easily reusable, high-level modules (so-called application building blocks)
  • changes to low-level modules should not affect high-level ones. So we have the possibility to change the behavior without modifying a big part of the application

So to summarize:

  • a high-level module must depend on an abstraction (define it – create an interface)
  • a low-level module must also depend on the same abstraction (implement it – provide an interface implementation)

Examples

The simplest example is Pipe in Angular. If it wasn’t for this interface, we wouldn’t be able to add custom Pipes to our applications (because then we’d have to add an IF handler for our particular Pipe in the Angular code).

High-level module: Angular – depends on abstraction (defines interface)

Low-level module: our application – implements the abstraction (implements the interface)

Another example.

Let’s suppose we have an ordering application for an online store. Therefore, we need to calculate the tax on the order.

The high-level module is a component with an injected service.

The low-level module is the service.

As long as our application runs within a single state, everything is simple. However, what if we would like to enter foreign markets? How do we calculate tax for different states?

 A naive solution:

A service that will return the appropriate value based on the country code submitted:

Problem: what if we want to serve another country in our application? We need to add “IF”.

Let’s try to think more abstract. After all, we are looking for a service that will calculate the tax for a given country. So, let’s create an interface that we will implement depending on the need:

PS This is the strategy design pattern ? .

Having separated the interface, we can move on to the implementation.

Now in the component, we will use the abstraction (interface) and not the concrete implementation.

Let’s look at how we can now provide an appropriate implementation at the module level.

Interesting fact:

What if we don’t know what implementation we want to use at the module level? I.e. we want to provide the implementation “on the fly”, in “Live” mode ?

Let’s suppose we get a country code as a parameter in a URL route.

So let’s define a factory that will provide us with appropriate implementations based on the country code we send:

We inject this factory into the component:

Let’s move on to the next example:

Let’s suppose that we have a service that does CRUD operations on an entity (in the form of HTTP requests):

What is wrong? At first glance, nothing.

The problem arises if we wanted to experimentally introduce GraphQL support in one of the environments. Then we would have to add an IF that checks the environment in each method:

Problem – we modify the existing code, initiating an environment check with IF. If we wanted to use e.g. WebSockets on yet another environment, we would add another IF, and another dependency to the service.

How do we solve this?

Let’s separate the interface:

Let’s change the usage from a concrete implementation to an abstraction (interface).

Let’s provide a concrete implementation depending on the environment at the module level:

This way we preserve the Dependency Inversion Principle. By the way, I also recommend an article that shows the behavior of this rule when combining Angular with NestJS – https://angular.love/en/2020/12/02/how-to-follow-the-dependency-inversion-principle-in-nestjs-and-angular/ 

About the author

Wojciech Janaszek

I am an Angular and NestJS developer. I started my adventure with the first versions of “new” Angular (2 and up). Recently I’m also using NestJS (which is easy to learn if you know Angular well – everything is very similar). In my work I pay a lot of attention to so called clean code and clean architecture. I like to have “order” in the code 🙂 In my free time I am interested in sports cars, motorsport in general. I also play amateur volleyball.

Don’t miss anything! Subscribe to our newsletter. Stay up-to-date with the latest trends, tips, meetups, courses and be a part of a thriving community. The job market appreciates community members.

Leave a Reply

Your email address will not be published. Required fields are marked *