DEV Community

Cover image for DIP - The Dependency Inversion Principle
Oliver Jumpertz
Oliver Jumpertz

Posted on • Updated on • Originally published at oliverjumpertz.com

DIP - The Dependency Inversion Principle

The Dependency Inversion Principle is a part of SOLID, a mnemonic acronym which bundles a total of 5 design principles.

It is often associated with clean code.

But what exactly is it, is it important to you, should you even care?

What does it state?

Modules that encapsulate high-level policy should not depend upon modules that implement details. Rather, both kinds of modules should depend upon abstractions.

This may sound a little complicated, but you can break it up, as follows:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces).
  2. Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

Still not better?

Okay, view it this way:

Don't let anything you build depend on a concrete implementation of something.
Better depend on an abstract description of a contract, and let any implementor decide how to satisfy that contract.

An Example

Looking at examples can usually help to understand a concept better, so here's one.

What you see here is one way of structuring a backend API.
A resource layer that implements the specifics of the API, a service which contains the business logic, and repositories which handle the persistence layer (e.g. database interactions).

class PositionRepository {
  constructor() {}
}

class TransactionRepository {
  constructor() {}
}

class TransactionService {
  private readonly transactionRepository: TransactionRepository;
  private readonly positionRepository: PositionRepository;

  constructor(
    transactionRepository: TransactionRepository,
    positionRepository: PositionRepository
  ) {
    this.transactionRepository = transactionRepository;
    this.positionRepository = positionRepository;
  }
}

class TransactionResource {
  private readonly transactionService: TransactionService;

  constructor(transactionService: TransactionService) {
    this.transactionService = transactionService;
  }
}
Enter fullscreen mode Exit fullscreen mode

If you take a closer look, you can see that the high level class (resource) depends on a concrete lower level class (service) which itself depends on two (more) lower level classes (repositories).

The control flow thus goes from left to right, from the highest to the lowest level.
It is like a Singly Linked List or even better a Tree. Every higher-level module references at least one lower-level module.
control_flow_before_dip.png

What does it try to prevent?

Whenever you couple a module (a class, a function, a whole module) to another module, that first module depends on a lot of lower-level detail.

In object-oriented systems this also means that you can only replace the logic you depend on by deriving from exactly this class and overwriting existing logic.
This is a pretty tight coupling, and a lot of inflexibility.

It becomes pretty difficult to replace lower-level logic, and changes to the lower-level logic might have a larger impact on higher-level one.

Just imagine you'd now restructure the TransactionService from the example.
You couldn't tell, without taking an explicit look at the TransactionResource, that you maybe now have to also make adjustments there.

Another issue is the direct dependency on the lower-level module.
If you apply this example to a library, and class A depends on class B, and both originate from different modules, a user would always have a dependency on module A and module B (transitive).
A user couldn't even choose to not take module B, because the direct relationship and thus a dependency exists.

Fixing the issues

You can inverse the control flow by letting each concrete implementation either depend on an interface or implement said interface, like seen below.

interface PositionRepository {}

interface TransactionRepository {}

interface TransactionService {}

class TransactionServiceImplementation implements TransactionService {
  constructor(
    transactionRepository: TransactionRepository,
    positionRepository: PositionRepository
  ) {}
}

class TransactionResource {
  private readonly transactionService: TransactionService;

  constructor(transactionService: TransactionService) {
    this.transactionService = transactionService;
  }
}
Enter fullscreen mode Exit fullscreen mode

Instead of the control flow going from the highest to the lowest level modules, there is now an abstraction in-between them.
control_flow_after_dip.png

Before you shout it out loud: yes, this adds more code overall, as each interface also needs to be described first.

But do you remember the issue with module dependencies?

Instead of module A now also requiring module B, module B now requires module A (given module A defines the interface), and this gives users the choice whether they want to pull in module B, as well, or want module C as an alternative to provide another implementation of the interface, which might be a better option for their use case!

Should you care?

Well, it depends.

In dynamically typed languages like JavaScript, you already depend on interfaces pretty much everywhere, as you can simply call any method or property on any object.
As long as a user provides everything you require, you're good to go!

But as soon as static typing comes into play, you should indeed care.
You gain all benefits described here, and it'll improve your code quality, usability, and maintainability a lot.

In the end the benefits weigh pretty heavy, and should not be ignored.

Before you leave

If you like my content, visit me on Twitter, and perhaps you’ll like what you see!

Top comments (0)