O Sertão será Cloud

Unveiling the magic of Dependency Inversion

What is Dependency Injection?

Dependency Injection is a design pattern in which a class’s dependencies are provided to it through constructors, configuration methods, or directly into properties.

Implementing Dependency Injection in TypeScript

Step 1: Defining the Classes

Let’s start by defining our main classes: User e SoftwareEngineer.

// User.ts
export class User {
private name: string;

constructor(name: string) {
this.name = name;
}

public getName(): string {
return this.name;
}
}

// SoftwareEngineer.ts
export class SoftwareEngineer {
private title: string;

constructor() {
this.title = 'Software Developer';
}

public getTitle(): string {
return this.title;
}
}

Step 2: Applying Dependency Injection

To apply DI, we’ll inject the SoftwareEngineer class into the User class through the constructor.

// User.ts
export class User {
private name: string;

constructor(name: string) {
this.name = name;
}

public getName(): string {
return this.name;
}
}

// SoftwareEngineer.ts
export class SoftwareEngineer {
private title: string;

constructor() {
this.title = 'Software Developer';
}

public getTitle(): string {
return this.title;
}
}

Dependency injection is a powerful technique for improving code modularity, testability, and maintenance in TypeScript. By avoiding direct coupling between classes, we can create more flexible and understandable systems. In this article, we explore from basic concepts to advanced implementations, using pure TypeScript and practical examples involving the User andSoftwareEngineer classes.

Step 3: Validating decoupling

To test the use of this functionality, let’s check the code below:

// User.test.ts
import { describe, expect, it } from '@jest/globals';
import { User } from './User';
import { SoftwareEngineer } from './SoftwareEngineer';

describe('User', () => {
it('should create a User instance', () => {
const jobMock = new SoftwareEngineer();
const user = new User('John Doe', jobMock);

expect(user.getName()).toBe('John Doe');
});

it('should get the job title of the User', () => {
const jobMock = new SoftwareEngineer();
const user = new User('John Doe', jobMock);

expect(user.getJobTitle()).toBe('Software Developer');
});
});

The result will be as follows:

Step 3: The difference between Dependency Injection and Inversion of Dependency

The big problem with the code is that the job should not be directly injected as a class, which requires modifying the User structure to change the user’s job. In the example below, we create a new class called SoftwareArchitect to return the respective job.

// SoftwareArchitect.ts
export class SoftwareArchitect {
private title: string;

constructor() {
this.title = 'Software Architect';
}

public getTitle(): string {
return this.title;
}
}

Step 3: Applying Inversion of Dependency

To apply Inversion of Dependency, we should consider that

“High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.” — Robert C. Martin’s.

Our injections should be based on interfaces rather than concrete classes. In the code below, we demonstrate the creation of the interface:

// JobInterface.ts
export interface JobInterface {
getTitle(): string
}

Now we need to update our classes to implement the interface:

// SoftwareEngineer.ts
import { JobInterface } from "./JobInterface";

export class SoftwareEngineer implements JobInterface {
private title: string;

constructor() {
this.title = 'Software Developer';
}

public getTitle(): string {
return this.title;
}
}

// SoftwareArchitect.ts
import { JobInterface } from "./JobInterface";

export class SoftwareArchitect implements JobInterface {
private title: string;

constructor() {
this.title = 'Software Architect';
}

public getTitle(): string {
return this.title;
}
}

And we should update the User class by declaring job with JobInterface:

// User.ts
import { JobInterface } from "./JobInterface";

export class User {
private name: string;
private job: JobInterface;

constructor(name: string, job: JobInterface) {
this.name = name;
this.job = job;
}

public getName(): string {
return this.name;
}

public getJobTitle(): string {
return this.job.getTitle();
}
}

With this, we can test with different classes:

import { describe, expect, it } from '@jest/globals';
import { User } from './User';
import { SoftwareEngineer } from './SoftwareEngineer';
import { SoftwareArchitect } from './SoftwareArchitect';

describe('User', () => {
it('should create a User instance', () => {
const jobMock = new SoftwareEngineer();
const user = new User('John Doe', jobMock);

expect(user.getName()).toBe('John Doe');
});

it('should get the job title of the User using SoftwareEngineer', () => {
const jobMock = new SoftwareEngineer();
const user = new User('John Doe', jobMock);

expect(user.getJobTitle()).toBe(jobMock.getTitle());
});

it('should get the job title of the User using SoftwareArchitect', () => {
const jobMock = new SoftwareArchitect();
const user = new User('John Doe', jobMock);

expect(user.getJobTitle()).toBe(jobMock.getTitle());
});
});

So what is Dependency Injection?

We consider Inversion of Dependency as a software design principle that suggests high-level modules should not directly depend on low-level modules. Instead, both should depend on abstractions, allowing greater flexibility and ease of code maintenance. Abstractions should not be affected by implementation details, which in turn should depend on abstractions to function effectively and decoupled. In summary, Inversion of Dependency promotes a more modular, resilient to change, and easily extendable design.


Unveiling the magic of Dependency Inversion was originally published in Stackademic on Medium, where people are continuing the conversation by highlighting and responding to this story.