Injeção de dependência na unha
O Caixa do mercadinho precisa do estoque para vender, e a versão ingênua
resolve isso por conta própria:
public class Caixa {
private final RepositorioEmArquivo repositorio = new RepositorioEmArquivo(Path.of("estoque.txt"));
public Resultado vender(String codigo, int quantidade) {
Produto produto = repositorio.buscar(codigo);
// baixa o estoque, grava, devolve o resultado
}
}
Compila, funciona, e cobra em dois lugares que este livro já preparou.
O primeiro é o teste: o CaixaTest não tem como testar essa
classe sem tocar o disco de verdade, porque o new do repositório está
cravado dentro dela, fora de alcance. O segundo é a troca: no dia em que o
estoque for para outro lugar, o apêndice de banco de dados é esse dia, cada
classe que cravou new RepositorioEmArquivo precisa ser editada. O Caixa
sabe demais: além do próprio trabalho, vender, ele decidiu de onde vêm os
dados e como o acesso é construído. Este capítulo tira dele essa segunda
responsabilidade.
Injeção pelo construtor
O desenho tem três passos, todos já ensinados. Primeiro, a dependência vira contrato, uma interface:
public interface RepositorioDeProdutos {
Produto buscar(String codigo);
void salvar(Produto produto);
}
Segundo, as implementações cumprem o contrato: a
RepositorioEmArquivo, que é a RepositorioDeEstoqueEmArquivo da prática
do capítulo 21 rebatizada com o nome no tamanho do contrato, e uma irmã de
laboratório, a RepositorioEmMemoria, um HashMap por dentro, sem disco
nenhum.
Terceiro, o passo que dá nome ao capítulo: o Caixa para de criar e passa a
receber.
public class Caixa {
private final RepositorioDeProdutos repositorio;
public Caixa(RepositorioDeProdutos repositorio) {
this.repositorio = repositorio;
}
// vender continua igual
}
Isso é injeção de dependência: as dependências de um objeto entram prontas por fora, pelo construtor, em vez de serem criadas por dentro. Nenhum mecanismo novo participa, é um construtor comum recebendo uma interface, e o efeito no teste é imediato:
@Test
void vendaBaixaOEstoque() {
RepositorioDeProdutos repositorio = new RepositorioEmMemoria();
repositorio.salvar(new Produto("7891000100103", "Café 500g", new BigDecimal("19.90"), 25));
Caixa caixa = new Caixa(repositorio);
caixa.vender("7891000100103", 2);
assertEquals(23, repositorio.buscar("7891000100103").estoque());
}
O teste roda em memória, em milissegundos, sem arquivo e sem limpeza,
porque a fixture escolhe a implementação. A RepositorioEmMemoria é um
dublê (test double): uma implementação que cumpre o contrato só para
servir ao teste; o sistema em produção recebe a de
arquivo, e o Caixa é o mesmo bytecode nos dois mundos, sem saber em qual
está. Testabilidade e trocabilidade são a mesma propriedade olhada de dois
lados, e as duas nascem do mesmo gesto: quem usa não constrói.
Existem outras duas formas de entregar a dependência, e a comparação entre
as três cai em entrevista. A injeção por setter entrega depois da
construção, por um método como setRepositorio(...); a injeção por campo
grava direto no campo privado, por reflexão, sem construtor e sem setter, e
é a forma que as anotações de framework popularizaram. As duas cobram o
mesmo preço: entre o new e a entrega existe um objeto pela metade, com a
dependência nula, e a classe perde o direito de declarar o campo final,
porque o campo passa a receber valor depois. O construtor não tem esse
intervalo e não abre essa exceção, e é por isso que a recomendação, feita
inclusive pelos próprios frameworks, é a injeção por construtor:
dependência obrigatória entra no construtor, e o setter fica para o que é
de fato opcional.
Meses depois, alguém acrescenta ao Caixa um relatório de fim de dia, e na
pressa resolve a dependência ali mesmo:
public void fecharDia() {
RepositorioEmArquivo historico = new RepositorioEmArquivo(Path.of("estoque.txt"));
// grava o resumo do dia por cima do estoque
}
O CaixaTest continua inteiro verde. O que ele deixou de proteger?
Tudo que passa por fecharDia: o new escondido ignora o repositório
injetado, e o método fala com o arquivo de verdade mesmo quando o teste
acredita estar em memória. Na melhor hipótese o teste do fechamento falha em
máquina alheia por falta do arquivo; na pior, a suíte roda na máquina do
mercadinho e escreve por cima do estoque real, um teste verde com efeito
colateral em produção. Dependência escondida é exatamente isso, uma
dependência que não aparece no construtor, e a disciplina que a armadilha
compra é curta: se a classe precisa, o construtor declara; new de
dependência dentro de método é sinal de revisão reprovada.
Inversão de controle e o grafo de objetos
O gesto tem um nome maior. Inversão de controle é o padrão em que a classe deixa de controlar a criação e a escolha das próprias dependências, e esse controle sobe para quem a monta. Cada classe fica sabendo menos, o acoplamento cai, e sobra a pergunta prática: se ninguém cria as próprias dependências, quem cria todas?
A resposta é o ponto de montagem. Todo sistema com injeção tem um lugar,
tipicamente o main, onde os objetos são criados e conectados de uma vez:
public static void main(String[] args) throws IOException {
Path arquivo = args.length > 0 ? Path.of(args[0]) : Path.of("estoque.txt");
RepositorioDeProdutos repositorio = new RepositorioEmArquivo(arquivo);
Caixa caixa = new Caixa(repositorio);
CadernetaDeFiado caderneta = new CadernetaDeFiado(repositorio, new BigDecimal("200.00"));
TelaDoTerminal tela = new TelaDoTerminal(caixa, caderneta);
tela.rodar();
}
O que esse trecho constrói chama-se grafo de objetos: o conjunto dos
objetos do sistema e das referências entre eles, montado uma vez na
partida. O repositório entra em dois lugares e existe uma vez; a tela
conhece o caixa, o caixa conhece o repositório, e ninguém conhece mais do
que o próprio construtor pede. O main vira o único lugar do sistema onde
as decisões de composição moram, e trocar o arquivo por memória, ou por
qualquer implementação futura, é editar essas linhas e mais nenhuma.
Um container na unha
Num sistema grande, o main de montagem cresce, e a ordem das criações
vira quebra-cabeça. O capítulo 23 deu as peças para automatizar: um
container de injeção é o objeto que sabe construir o grafo sozinho, criando
cada tipo pedido e resolvendo os parâmetros de construtor recursivamente.
Um funcional cabe numa tela:
public class Container {
private final Map<Class<?>, Class<?>> vinculos = new HashMap<>();
private final Map<Class<?>, Object> prontos = new HashMap<>();
public void vincular(Class<?> contrato, Class<?> implementacao) {
vinculos.put(contrato, implementacao);
}
public void registrar(Class<?> tipo, Object valor) {
prontos.put(tipo, valor);
}
public <T> T resolver(Class<T> tipo) {
if (prontos.containsKey(tipo)) {
@SuppressWarnings("unchecked")
T pronto = (T) prontos.get(tipo);
return pronto;
}
try {
Class<?> alvo = vinculos.getOrDefault(tipo, tipo);
Constructor<?>[] construtores = alvo.getDeclaredConstructors();
if (construtores.length == 0) {
throw new IllegalStateException("Não sei construir " + tipo.getName());
}
Constructor<?> construtor = construtores[0];
Object[] argumentos = new Object[construtor.getParameterCount()];
Class<?>[] tiposDosParametros = construtor.getParameterTypes();
for (int i = 0; i < argumentos.length; i++) {
argumentos[i] = resolver(tiposDosParametros[i]);
}
Object instancia = construtor.newInstance(argumentos);
prontos.put(tipo, instancia);
return tipo.cast(instancia);
} catch (ReflectiveOperationException erro) {
throw new IllegalStateException("Não sei construir " + tipo.getName(), erro);
}
}
}
A leitura: vincular registra qual implementação atende cada contrato;
registrar guarda um valor já pronto, porque valor de configuração, um
caminho, um limite, não se constrói por construtor; resolver olha o
construtor da implementação, resolve cada parâmetro chamando a si mesmo,
recursivamente sobre o grafo, cria com o newInstance e guarda. A
conferência do array vazio pega o esquecimento clássico, o contrato sem
vincular, cujo alvo é uma interface sem construtor nenhum; o catch de
ReflectiveOperationException, a superclasse das exceções da reflexão,
embrulha o resto. O main encolhe para as decisões e um pedido:
Container container = new Container();
container.vincular(RepositorioDeProdutos.class, RepositorioEmArquivo.class);
container.registrar(Path.class, Path.of("estoque.txt"));
container.registrar(BigDecimal.class, new BigDecimal("200.00"));
TelaDoTerminal tela = container.resolver(TelaDoTerminal.class);
tela.rodar();
O caixa e a caderneta dependem os dois de RepositorioDeProdutos, e cada
um ganhou um acessor repositorio() de laboratório, só para a comparação:
Caixa caixa = container.resolver(Caixa.class);
CadernetaDeFiado caderneta = container.resolver(CadernetaDeFiado.class);
IO.println(caixa.repositorio() == caderneta.repositorio());
true ou false, e por quê?
true
O mapa prontos guarda cada instância criada e a reaproveita: dois pedidos
do mesmo tipo recebem o mesmo objeto, a mesma referência. Esse
regime tem nome, singleton: uma única instância de um tipo, compartilhada
por todo o sistema, e para um repositório é o regime certo, porque dois
repositórios do mesmo arquivo seriam dois caches brigando. O nome também
batiza um anti-padrão famoso, o singleton por campo estático global, que
recria o problema da armadilha, dependência escondida e intestável, com o
agravante do estado compartilhado; a diferença entre os dois
usos é quem controla, o container, visível e trocável, ou a própria classe,
escondida.
O que os frameworks acrescentam. O container de uma tela resolve
construtores; os de mercado somam o escaneio por anotações
para dispensar o vincular manual, escopos além do singleton, configuração
externa e diagnósticos de grafo. O mecanismo central, resolver construtores
recursivamente e cachear, é o desta página, e quem o escreveu uma vez lê
qualquer stack trace de container sem susto.
Prática
-
Extraia a interface
RepositorioDeProdutosdo mercadinho, implemente a versão em memória, e convertaCaixaeCadernetaDeFiadopara injeção pelo construtor. Meça o tempo da suíte de testes antes e depois de trocar o repositório dos testes para a memória. -
Reproduza a armadilha do
newescondido: faça o teste do fechamento passar enquanto grava num arquivo real, prove o efeito colateral, e conserte injetando. Escreva a regra de revisão em uma frase. -
Implemente o
Containercompleto e monte o mercadinho com ele. Depois vinculeRepositorioDeProdutosà memória e rode a suíte inteira pelo container, sem editar nenhuma classe de domínio. -
Acrescente ao container a detecção de ciclo:
Aque depende deBque depende deAhoje estoura a pilha de chamadas. Transforme o estouro numaIllegalStateExceptioncom a cadeia de tipos na mensagem. -
Escreva em um parágrafo, com o vocabulário deste capítulo, por que o relógio injetável do capítulo 20 é o mesmo padrão, e injete um
Clockfixo naCadernetaDeFiadopara testar o vencimento do fiado sem esperar trinta dias.
Ficha do capítulo
| Termo | Definição |
|---|---|
| injeção de dependência | dependências entram prontas pelo construtor, criadas por fora |
| injeção por setter | dependência entregue depois da construção; objeto pela metade no meio |
| inversão de controle | a criação e a escolha das dependências sobem para quem monta |
| grafo de objetos | os objetos do sistema e as referências entre eles, montados na partida |
| container | o objeto que constrói o grafo: resolve construtores recursivamente e cacheia |
| singleton | uma instância única compartilhada; regime do container, anti-padrão quando estático e escondido |
| Regra prática | |
|---|---|
| dependência | se a classe precisa, o construtor declara; new interno é dependência escondida |
| ponto de montagem | um só: o main ou o container; decisões de composição moram juntas |
| teste | dublê em memória via injeção; disco e relógio de verdade só onde são o assunto |