Conceito de Injeção de Dependência e sua importância no desenvolvimento de software.
A Injeção de Dependência (ou Dependency Injection em inglês) é um padrão de design utilizado no desenvolvimento de software para facilitar a manutenção, teste e extensibilidade de um sistema.
Em termos simples, isso significa que uma classe não deve criar as instâncias das classes que ela precisa para realizar suas operações.
Em vez disso, essas dependências são fornecidas externamente, geralmente por meio de construtores, métodos de configuração ou através de um container de inversão de controle (IoC container).
Seus benefícios são:
Desacoplamento: A Injeção de Dependência ajuda a reduzir o acoplamento entre classes, pois as classes não precisam se preocupar com a criação ou gestão de suas próprias dependências. Isso facilita a manutenção do código e torna as classes mais fáceis de entender e reutilizar.
Testabilidade: Ao passar as dependências externamente, é mais fácil substituir essas dependências por mocks ou stubs durante os testes unitários. Isso torna os testes mais simples de escrever e menos propensos a depender de componentes externos.
Flexibilidade: A Injeção de Dependência permite que as dependências sejam trocadas facilmente. Se uma nova implementação de uma dependência for necessária, ela pode ser facilmente substituída sem alterar o código da classe que a utiliza.
Agora vamos ao código...
Apresentando as classes de exemplo
Service de Restaurants sem uso da D.I
import { Repository } from 'typeorm'
import { Restaurant } from '../entities/restaurant'
import { restaurantRepository } from './repositories/restaurantRepo'
const newRestaurantService = new RestaurantService(restaurantRepository)
interface IRestaurantService {
listAllRestaurants(): Promise<Restaurant[]>
}
class RestaurantService implements IRestaurantService {
listAllRestaurants = async (): Promise<Restaurant[]> => {
const allRestaurantsService = await restaurantRepository.find()
return allRestaurantsService
}
}
Controller de Restaurants sem uso da D.I
import { NextFunction, Request, Response } from 'express'
import { RestaurantService } from './services/RestaurantService'
const restaurantService = new RestaurantService()
class RestaurantController {
async getAllRestaurants(req: Request, res: Response, next: NextFunction) {
try {
const allRestaurants = await restaurantService.listAllRestaurants()
return res.status(200).json(allRestaurants)
} catch (error) {
next(error)
}
}
}
Problemas dessa abordagem
Utilizar essa abordagem é comum em projetos pessoais, quando estamos começando a programar, pois ela "funciona", mas trará alguns problemas no longo prazo como:
Acoplamento forte: A classe RestaurantController está diretamente instanciando a classe RestaurantService, o que cria um forte acoplamento entre essas duas classes. Isso significa que se a implementação do RestaurantService mudar no futuro, será necessário modificar também o RestaurantController.
Dificuldade de teste: Como o RestaurantController está criando diretamente uma instância de RestaurantService, é difícil testar o RestaurantController de forma isolada. Isso porque não há como substituir a instância de RestaurantService por um mock ou stub durante os testes.
Testes configuram um parte importante do desenvolvimento de software, entretanto quando estamos começando a programar não conseguimos notar a importância dos testes devido ao tamanho dos nossos projetos.
Infelizmente, muitas vezes descobrimos a necessidade dos testes quando tudo está em chamas.
Em resumo, o alto acoplamento pode tornar a escrita de testes mais difícil, tediosa e menos eficaz, o que pode desencorajar os desenvolvedores a investir tempo e esforço na criação de uma base de testes robusta.Reutilização limitada: A classe RestaurantController está diretamente ligada à implementação específica de RestaurantService, o que limita a capacidade de reutilizar o RestaurantController com diferentes implementações de serviços de restaurante ou em contextos diferentes.
Refatorando o código
RestaurantService refatorado com D.I
import { Repository } from 'typeorm'
import { Restaurant } from '../entities/restaurant'
interface IRestaurantService {
listAllRestaurants(): Promise<Restaurant[]>
}
class RestaurantService implements IRestaurantService {
private restaurantRepo: Repository<Restaurant>
constructor(restaurantRepo: Repository<Restaurant>) {
this.restaurantRepo = restaurantRepo
}
listAllRestaurants = async (): Promise<Restaurant[]> => {
const allRestaurantsService = await this.restaurantRepo.find()
return allRestaurantsService
}
}
export { RestaurantService, IRestaurantService }
RestaurantController refatorado com D.I
import { NextFunction, Request, Response } from 'express'
import { IRestaurantService } from '../services/Restaurant.service'
class RestaurantController {
restaurantService: IRestaurantService
constructor(restaurantService: IRestaurantService) {
this.restaurantService = restaurantService
}
getAllRestaurants = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const allRestaurants = await this.restaurantService.listAllRestaurants()
return res.status(200).json(allRestaurants)
} catch (error) {
next(error)
}
}
}
Dessa forma, a classe RestaurantController não é mais responsável por criar uma instância de RestaurantService, mas sim por recebê-la como uma dependência externa. Assim como, RestaurantService recebe o repositório de uma dependência externa. Isso melhora a modularidade, flexibilidade e testabilidade do código.
Para instanciá-los ficaria dessa maneira, já passando para uma rota do express:
const newRestaurantService = new RestaurantService(restaurantRepository)
const { getAllRestaurants } = new RestaurantController(newRestaurantService)
const router = express.Router()
router.get('/allrestaurants', getAllRestaurants)
Conclusão
O alto acoplamento entre os componentes de um sistema pode representar um grande obstáculo para os desenvolvedores ao escreverem testes eficazes. Esse acoplamento dificulta a isolamento das dependências, substituição de componentes e torna os testes mais lentos e frágeis. Como resultado, os desenvolvedores podem ser desencorajados a investir tempo na escrita de testes, o que compromete a qualidade do código e a capacidade de evolução do sistema. Portanto, é crucial reduzir o acoplamento entre os componentes do sistema, adotando práticas como a Injeção de Dependência, para facilitar a escrita de testes e garantir a robustez e manutenção do software.
Top comments (0)