Desvendando a magia da Inversão de dependência
O que é Injeção de Dependência?
A injeção de dependência é um padrão de design no qual as dependências de uma classe são fornecidas a ela por meio de construtores, métodos de configuração ou diretamente em propriedades.
Implementando Injeção de Dependência em TypeScript
Passo 1: Definindo as Classes
Vamos começar definindo nossas classes principais: 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;
}
}
Passo 2: Aplicando Injeção de Dependência
Para aplicar DI, vamos injetar a classe SoftwareEngineer
na classe User
através do construtor.
// User.ts
import { SoftwareEngineer } from './SoftwareEngineer';
export class User {
private name: string;
private job: SoftwareEngineer;
constructor(name: string, job: Job) {
this.name = name;
this.job = job;
}
public getName(): string {
return this.name;
}
public getJobTitle(): string {
return this.job.getTitle();
}
}
A injeção de dependência é uma técnica poderosa para melhorar a modularidade, testabilidade e manutenção do código em TypeScript. Ao evitar o acoplamento direto entre classes, podemos criar sistemas mais flexíveis e fáceis de entender. Neste artigo, exploramos desde os conceitos básicos até implementações avançadas, utilizando TypeScript puro e exemplos práticos envolvendo as classes User
e SoftwareEngineer
.
Passo 3: Validando o desacoplamento
Visando testar o uso dessa funcionalidade, vamos verificar o código abaixo:
// 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');
});
});
O resultado será esse:
Passo 3: A diferença entre Injeção de Dependência e Inversão de Dependência
O grande problema do código é que o Job não deveria está condicionada a uma injeção direta de Classe, isso faz com que tenhamos de modificar a estrutura do User para modificar o emprego do usuário. No exemplo abaixo criamos uma nova classe chamada SoftwareArchitect
que deve retorna o respectivo trabalho.
// SoftwareArchitect.ts
export class SoftwareArchitect {
private title: string;
constructor() {
this.title = 'Software Architect';
}
public getTitle(): string {
return this.title;
}
}
Passo 3: Aplicando os conceitos de Inversão de Dependência
Para aplicar a Inversão de Dependência devemos pensar que:
”Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações. Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.” Robert C. Martin’s
Dessa forma podemos concluir que nossas injeções devem-se basear em interfaces e não em classes concretas. No código abaixo mostraremos a criação da interface:
// JobInterface.ts
export interface JobInterface {
getTitle(): string
}
Agora devemos atualizar às nossas classes implementando a 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;
}
}
E devemos atualizar a classe User
declarando jobs com 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();
}
}
Com isso podemos fazer os testes com diferentes 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());
});
});
Então o que é Inversão de Dependência?
Consideramos a inversão de dependência é um princípio de design de software que propõe que módulos de alto nível não devam depender diretamente de módulos de baixo nível. Em vez disso, ambos devem depender de abstrações, permitindo uma maior flexibilidade e facilidade de manutenção do código. Abstrações não devem ser afetadas por detalhes de implementação, que devem, por sua vez, depender das abstrações para funcionar de maneira eficaz e desacoplada. Em resumo, a inversão de dependência promove um design mais modular, resiliente as mudanças e fácil de estender.