Texto de: Geraldo Daros
Introdução
A injeção e a inversão de dependência são princípios cruciais para o desenvolvimento de software eficiente e de alta qualidade. Esses conceitos ajudam a criar sistemas mais modulares, flexíveis e fáceis de testar, permitindo uma melhor separação de preocupações e reduzindo o acoplamento entre componentes. Neste artigo, vamos explorar como aplicar a injeção e a inversão de dependência mediante exemplos práticos em Java, demonstrando como essas técnicas podem transformar a arquitetura do seu código e melhorar sua manutenção e extensibilidade.
Injeção de dependências
A injeção de dependência é uma técnica na qual uma classe recebe suas dependências de uma fonte externa, em vez de criá-las internamente. Isso significa que a responsabilidade pela criação e gerenciamento dessas dependências é transferida para a entidade que faz a injeção. Vamos ver como isso funciona na prática com um exemplo de CRUD utilizando um banco de dados MySQL.
Primeiro, vamos criar uma classe para juntar todos os comportamentos relativos ao acesso ao banco de dados. Vou chamar essa classe aqui de MySQL e deixarei os métodos com implementações de teste só para facilitar o exemplo:
public class MySQL {
public void inserir() {
System.out.println("Inserindo com MySQL");
}
public void atualizar() {
System.out.println("Atualizando com MySQL");
}
public void excluir() {
System.out.println("Excluindo com MySQL");
}
public void buscar() {
System.out.println("Buscando com MySQL");
}
}
Depois disso, vamos querer acessar esses comportamentos da classe MySQL em outra classe, que vou chamar de Servico
.
public class Servico {
private final MySQL mySQL;
public Servico(MySQL mySQL) {
this.mySQL = mySQL;
}
public void criar() {
this.mySQL.inserir();
}
public void buscarTodos() {
this.mySQL.buscar();
}
public void atualizar() {
this.mySQL.atualizar();
}
public void excluir() {
this.mySQL.excluir();
}
}
Após definir essa classe Servico
, podemos chamá-la e acessar seus métodos da seguinte forma:
public class Main {
public static void main(String[] args) {
Servico servico = new Servico(new MySQL());
servico.criar();
servico.buscarTodos();
servico.atualizar();
servico.excluir();
}
}
Resultado:
Inserindo com MySQL
Buscando com MySQL
Atualizando com MySQL
Excluindo com MySQL
Nesse exemplo, estamos injetando uma dependência. Criamos a classe MySQL
para gerenciar a conexão com o banco de dados e realizamos as operações CRUD. Em seguida, injetamos uma instância dessa classe na nossa classe Servico
via construtor. Isso permite que Servico
interaja com MySQL
sem precisar conhecer detalhes específicos sobre a implementação do banco de dados.
Essa é uma forma interessante de trabalhar com as dependências, porém, se precisarmos usar outro banco de dados, como PostgreSQL
, nosso código está fortemente acoplado ao MySQL
. Como podemos resolver isso? Usando inversão de dependência!
Inversão de Dependência
A inversão de dependência é um princípio de design que sugere que módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações. Além disso, abstrações não devem depender de detalhes; os detalhes devem depender de abstrações. Em termos mais simples, ao invés de um objeto criar suas dependências, ele deve receber as dependências de uma fonte externa.
Agora é que a “mágica” acontece!
Para aplicar a inversão de dependência, criaremos uma interface chamada BancoDeDados
que abstrai as operações de um banco de dados:
public interface BancoDeDados {
void inserir();
void atualizar();
void excluir();
void buscar();
}
Essa interface define um contrato que todas as implementações de banco de dados devem seguir. Agora, criaremos uma classe que implementa essa interface para o MySQL, chamada MySQLImpl
:
public class MySQLImpl implements BancoDeDados {
@Override
public void inserir() {
System.out.println("Inserindo com MySQL");
}
@Override
public void atualizar() {
System.out.println("Atualizando com MySQL");
}
@Override
public void excluir() {
System.out.println("Excluindo com MySQL");
}
@Override
public void buscar() {
System.out.println("Buscando com MySQL");
}
}
Agora, nós temos uma classe conectada ao banco de dados MySQL e segue as regras do nosso BancoDeDados
. Até agora, não usamos nenhum tipo de inversão de dependência, estávamos “preparando o terreno”.
A inversão de dependência é, ao invés de o nosso Servico
precisar de uma implementação da classe MySQL
, ele dependerá de uma abstração, no nosso caso, nossa abstração BancoDeDados
. E essa é a inversão de dependência, não é a nossa classe que depende de uma implementação, é a abstração que depende de uma implementação. Com isso, conseguimos desacoplar nosso serviço da implementação específica do banco de dados. Em vez de nosso serviço saber diretamente como interagir com o MySQL, ele interage com a abstração BancoDeDados
, permitindo que troquemos a implementação do banco de dados sem alterar o Servico
.
Refatorando a classe Servico
, ela ficará assim:
public class Servico {
private final BancoDeDados db;
public Servico(BancoDeDados db) {
this.db = db;
}
public void criar() {
this.db.inserir();
}
public void buscarTodos() {
this.db.buscar();
}
public void atualizar() {
this.db.atualizar();
}
public void excluir() {
this.db.excluir();
}
}
Agora nossa classe Servico
depende de uma classe que implementa da interface BancoDeDados
. Quando formos usar nosso Servico
, precisaremos passar a nova classe MySQLImpl
para que então funcione com o novo código. Dessa forma:
Servico servico = new Servico(new MySQLImpl());
Perfeito, mas, até agora, isso não mudou nada! Só deu mais trabalho, criar interface, depender de abstração, criar uma classe Impl. Para que tudo isso?
Agora podemos usar o Servico
com qualquer banco de dados que implemente BancoDeDados
. Vamos criar uma implementação para o PostgreSQL:
public class PostgresImpl implements BancoDeDados {
@Override
public void inserir() {
System.out.println("Inserindo com Postgres");
}
@Override
public void atualizar() {
System.out.println("Atualizando com Postgres");
}
@Override
public void excluir() {
System.out.println("Excluindo com Postgres");
}
@Override
public void buscar() {
System.out.println("Buscando com Postgres");
}
}
Usando ambas implementações:
public class Main {
public static void main(String[] args) {
System.out.println("Usando implementação MySQL");
Servico servicoComMySQL = new Servico(new MySQLImpl());
servicoComMySQL.criar();
servicoComMySQL.buscarTodos();
servicoComMySQL.atualizar();
servicoComMySQL.excluir();
System.out.println("\\nUsando implementação Postgres");
Servico servicoComPostgres = new Servico(new PostgresImpl());
servicoComPostgres.criar();
servicoComPostgres.buscarTodos();
servicoComPostgres.atualizar();
servicoComPostgres.excluir();
}
}
Resultado:
Usando implementação MySQL
Inserindo com MySQL
Buscando com MySQL
Atualizando com MySQL
Excluindo com MySQL
Usando implementação Postgres
Inserindo com Postgres
Buscando com Postgres
Atualizando com Postgres
Excluindo com Postgres
Essa abordagem permite que o nosso código fique mais modular, e traz diversos benefícios, especialmente quando pensamos em teste e manutenção da aplicação.
Conclusão
A injeção e a inversão de dependência são práticas essenciais para criar software robusto e flexível. Elas permitem a criação de código desacoplado, facilitam a manutenção e promovem a reutilização de componentes. Implementar essas práticas em projetos Java, mesmo sem frameworks, pode tornar seu código mais limpo, modular e fácil de testar.