Dependency Inversion Principle

Intro

Welcome back to another SOLID principles post. Previously, we have covered all four SOLID principles and in this post, we will end the story that began way back in April.

 

In this post, we will deep dive into the last principle which is the Dependency Inversion Principle. Therefore, get your coffee and let’s start. 

Real Life Example

To start with we will go through an example from our daily life. After, that we will go through mouth-watering technicalities.

Often than not a job contract will define the expected outcome of an employee without going through the details of how to achieve the expected outcome. For instance, you will find the following terms in a contract.

 

1- you are expected to participate in the development of web applications.

2- you are expected to be a team player. 

3- you are expected to drink coffee. 

 

All the above points abstract the company expectations of the employee without revealing how to achieve them. 

 

In this example, the company is considered as a high-level module and the employee is the low-level module, while the contract is the abstraction that both the company and the employee depends on. 

Class Levels

From the previous real-life example, we came to know that a job contract abstracts the relationship between a company and an employee. Let’s put that perspective into the programming world.

 

Assume you are building a backend for a web or mobile application.

 

In a real case scenario, this will mean the need for a variety of operations such as accepting and responding to requests, storing and retrieving data, input validation, business logic and more. 

 

The classes used in these operations can be grouped into High-level and  Low-level classes or modules

  • ⬆️  High-level classes are the classes that contain business logic. 
  • ⬇️  Low-level classes are the classes that deal with the underlying infrastructure such as database, networking, file system, just to mention a few.

The two levels of classes communicate back and forth with each other to complete a required operation (e.g. creating a new user account).

 

However, the high-level class does not call the low-level class directly. Instead, it deals with an interface that the low-level class implements. 

The Strategy Pattern meme

If you are confused, it is okay I was confused as well but it will get easier once we reach the code example. But before we code let’s get your tongue twisted with principle definition. 

Principle Definition

A) High-level classes shouldn’t be tightly coupled with low-level classes. Instead, both classes should depend on abstractions.

B) Abstractions shouldn’t depend on implementation. Implementation should depend on abstractions.

The above definition was inspired by Uncle Bob definition. But to be honest the definition sounds like a tongue twister until you understand it. To do so let’s do some coding through a case scenario. 

Case Scenario

Assuming that you have to develop a random string generator service. Bear in mind, the string generator implementation is likely to change. Therefore, design the code in a way that it will be easy to add new implementation without the need of changing the current implementation. 

 

For this case, I have chosen to go with typescript as the programming language.  Below is a simple diagram of the implementation.

 

Note: the final code link will be at the bottom of the post. Therefore, I want your focus on understanding the code and  not on copying and pasting. 

As we mentioned before both high-level and low-level classes should depend on abstraction, and that’s exactly what the diagram illustrates. 

  • RandomStringService is a high-level class with the sole purpose of triggering low-level classes using the IRandomString interface.
  • IRandomString is an interface that describes the required functionality.
  • Crypto is a low-level class that implements IRandomString using a crypto library
  • NanoID is a low-level class that implements IRandomString using the NanoID library. 

Let’s see the code in action.

First, let’s define the IRandomString interface. 

export interface IRandomString {
    randomString(): string;
    randomStringOfMinLength(stringMinLength: number) : string;
}

The interface has two simple methods. Both return a string. However, the second method expects a number that specifies the minimum random string length. 

Next, will create the implementations of IRandomString interface. 

import {IRandomString} from "../interfaces/randomString";
import {nanoid} from "nanoid";

export class NanoID implements IRandomString {
    randomString(): string {
        return nanoid();
    }

    randomStringOfMinLength(stringLength: number): string {
        return nanoid(stringLength);
    }
}

NanoID implementation is straightforward. The class implements the interface and returns a random string based on the nanoid library

import {IRandomString} from "../interfaces/randomString";
import * as crypto from "crypto";

export class Crypto implements IRandomString {

    randomString(): string {
        return crypto.randomBytes(32).toString("hex")
    }

    randomStringOfMinLength(stringLength: number): string {
        return crypto.randomBytes(stringLength)
        .toString("hex");
    }
}

Crypto implementation is a bit different. The logic of it is getting random bytes and convert them to a string in hex format. Here is the documentation of the crypto library

import {IRandomString} from "./interfaces/randomString";

export class RandomStringService {
    private randomStringGenerator: IRandomString;

    constructor(randomString: IRandomString) {
        this.randomStringGenerator = randomString
    }

    randomString(): string {
        return this.randomStringGenerator.randomString()
    }

    randomStringOfMinLength(stringLength: number) : string {
        return this.randomStringGenerator.randomStringOfMinLength(stringLength)
    }

}

RandomStringService implementation is fairly simple. It takes the IRandomString interface as a constructor parameter. In addition, the class have two methods which basically invokes the interface methods. 

If you are currently in WTH moment, don’t worry it will all make sense after the following code. 

import {Crypto} from "./utils/RandomString/implementation/crypto";
import {NanoID} from "./utils/RandomString/implementation/nanoID";
import {RandomStringService} from "./utils/RandomString/randomStringService";

function main() {
    
    let ranCrySrv = new RandomStringService(new Crypto())
    console.log(ranCrySrv.randomString())
    console.log(ranCrySrv.randomStringOfMinLength(10))

    let ranNanoSrv = new RandomStringService(new NanoID())
    console.log(ranNanoSrv.randomString())
    console.log(ranNanoSrv.randomStringOfMinLength(10))
}

main()

The above code is the entry point of this application. Notice, in line 7 a new RandomStringService is initiated with Crypto implementation and in line 11 with NanoID

 

Each time we invoked both methods just to confirm that the implementation works. But, what will happen if we swap NanoID() with Crypto() and vice versa? 

 

You guessed it, the code will work flawlessly with no issues and this is the magic of the dependency inversion principle. 

Conclusion

The dependency inversion principle is one of the most confusing principles at first glance. However, this principle is very effective for decoupling the layers of the project and plays well with other principles. 

 

Finally, I hope I made it crisp clear, and feel free to share your thoughts and questions in the comments. 

Support Ideas

If you liked the content and thought of supporting the creator, below are some ideas to do so.

  • your engagement in the comment will help giving more value to the content.
  • spreading the content in social media will help attract more readers and google ranking
Share on facebook
Share on twitter
Share on linkedin
Share on whatsapp
Share on email
You Can

Buy me a Coffee

  • also consider subscribing to the newsletter list

One comment

  1. […] impact COVID-19 has on our time (Todd Gerber) Signing GitHub Commits With YubiKey (Den Delimarsky) Dependency Inversion Principle (Anwar Al Jahwari) The Benefits Of Team Reflections And How To Lead One (Sarah Ribeiro) Why […]

Leave a Reply

Physical Address

304 North Cardinal St.
Dorchester Center, MA 02124