No Spring, injetar uma lista de Beans não é gambiarra
Existe uma reação comum quando alguém vê isso em um projeto Spring:
public PedidoService(List<ValidadorPedido> validadores) {
this.validadores = validadores;
}
A primeira impressão costuma ser:
"isso parece gambiarra".
Como se o correto fosse sempre escolher um único Bean com @Qualifier.
Como se receber uma lista fosse um jeito torto de escapar da ambiguidade.
Como se várias implementações do mesmo contrato fossem, por si só, sinal de bagunça.
Não é bem assim.
Injetar uma lista de Beans é um recurso legítimo do Spring.
E mais do que isso: em alguns desenhos, é exatamente o modelo certo.
O ponto importante é entender o que a lista representa.
Ela não deveria ser um truque para o container parar de reclamar.
Ela deveria representar uma colaboração real entre várias implementações do mesmo contrato.
O Spring consegue entregar todos os Beans de um tipo
Imagine uma aplicação que valida pedidos antes do fechamento.
Existe um contrato:
public interface ValidadorPedido {
void validar(Pedido pedido);
}
Uma implementação valida estoque:
@Component
public class ValidadorEstoque implements ValidadorPedido {
@Override
public void validar(Pedido pedido) {
// valida disponibilidade dos itens
}
}
Outra valida limite de crédito:
@Component
public class ValidadorCredito implements ValidadorPedido {
@Override
public void validar(Pedido pedido) {
// valida limite do cliente
}
}
Outra valida regras antifraude:
@Component
public class ValidadorAntifraude implements ValidadorPedido {
@Override
public void validar(Pedido pedido) {
// valida sinais de risco
}
}
Agora o serviço precisa executar todas essas validações:
@Service
public class FecharPedidoService {
private final List<ValidadorPedido> validadores;
public FecharPedidoService(List<ValidadorPedido> validadores) {
this.validadores = validadores;
}
public void fechar(Pedido pedido) {
validadores.forEach(validador -> validador.validar(pedido));
// fecha o pedido
}
}
Nesse caso, a lista não é um acidente.
Ela expressa o desenho.
O serviço não quer escolher um validador.
Ele quer executar todos os validadores registrados.
Essa diferença muda tudo.
Quando o ponto de injeção pede ValidadorPedido, ele pede um único colaborador.
Quando pede List<ValidadorPedido>, ele pede o conjunto de colaboradores daquele tipo.
O contrato mudou.
E esse contrato pode ser perfeitamente honesto.
Lista não é alternativa automática para Qualifier
Nos posts anteriores, a discussão passou por @Primary e @Qualifier.
Eles aparecem quando existem vários Beans compatíveis e um ponto de injeção precisa de apenas um.
Com lista, a intenção é outra.
O ponto de injeção não está dizendo:
"não sei qual escolher".
Ele está dizendo:
"eu preciso de todos".
Por isso, trocar uma ambiguidade por List<T> sem pensar costuma ser ruim.
Veja este código:
@Service
public class EnviarNotificacaoService {
private final List<EnviadorMensagem> enviadores;
public EnviarNotificacaoService(List<EnviadorMensagem> enviadores) {
this.enviadores = enviadores;
}
public void enviar(Notificacao notificacao) {
EnviadorMensagem enviador = enviadores.get(0);
enviador.enviar(notificacao);
}
}
Aqui a lista está escondendo uma escolha.
O serviço não precisa de todos os enviadores.
Ele está pegando o primeiro.
Isso é frágil porque transforma a ordem dos Beans em regra de negócio implícita.
Se o objetivo era enviar por e-mail, use uma dependência explícita.
Se o objetivo era escolher por canal, modele um resolvedor.
Se o objetivo era enviar por todos os canais, aí sim a lista pode fazer sentido.
O erro não está em List<EnviadorMensagem>.
O erro está em fingir que uma lista resolve uma decisão que o modelo não explicou.
A lista fica boa quando o comportamento é coletivo
Existem muitos casos em que várias implementações participam juntas do mesmo fluxo:
- validadores;
- filtros de domínio;
- enriquecedores de dados;
- políticas de cálculo;
- handlers de eventos;
- exportadores;
- regras de elegibilidade;
- etapas de um pipeline.
Nesses cenários, a lista representa composição.
Cada Bean implementa uma parte pequena e isolada do comportamento.
O componente orquestrador percorre o conjunto e aplica a colaboração.
Por exemplo:
public interface EnriquecedorPedido {
void enriquecer(Pedido pedido);
}
@Component
public class EnriquecedorCliente implements EnriquecedorPedido {
@Override
public void enriquecer(Pedido pedido) {
// adiciona dados do cliente
}
}
@Component
public class EnriquecedorEntrega implements EnriquecedorPedido {
@Override
public void enriquecer(Pedido pedido) {
// adiciona dados de entrega
}
}
E o uso:
@Component
public class PipelineEnriquecimentoPedido {
private final List<EnriquecedorPedido> enriquecedores;
public PipelineEnriquecimentoPedido(List<EnriquecedorPedido> enriquecedores) {
this.enriquecedores = enriquecedores;
}
public void executar(Pedido pedido) {
enriquecedores.forEach(enriquecedor -> enriquecedor.enriquecer(pedido));
}
}
Esse desenho é extensível sem precisar alterar o pipeline toda vez que surge uma nova etapa.
Uma nova implementação entra no container.
O Spring a inclui na lista.
O pipeline passa a considerar essa etapa.
Isso não é gambiarra.
É uma forma de usar o container como mecanismo de composição.
Mas essa liberdade cobra uma responsabilidade: o contrato precisa ser claro o suficiente para todas as implementações fazerem sentido dentro do mesmo conjunto.
Ordem precisa ser explícita quando ela importa
Um cuidado importante aparece quando a ordem de execução muda o resultado.
Se os validadores são independentes, a ordem talvez não importe.
Mas em um pipeline de enriquecimento, pode importar muito.
Imagine que uma etapa depende de dados adicionados por outra.
Nesse caso, confiar em uma ordem acidental é pedir problema.
O Spring permite ordenar Beans usando @Order ou implementando Ordered:
@Component
@Order(1)
public class EnriquecedorCliente implements EnriquecedorPedido {
}
@Component
@Order(2)
public class EnriquecedorEntrega implements EnriquecedorPedido {
}
Agora a ordem deixou de ser consequência invisível do bootstrap.
Ela virou parte declarada do desenho.
Mesmo assim, vale cuidado.
Se uma etapa depende demais da anterior, talvez o contrato esteja genérico demais.
Uma lista ordenada funciona bem quando existe uma sequência conceitual clara.
Ela fica perigosa quando vira uma cadeia de efeitos colaterais que só funciona porque as classes foram executadas em uma ordem específica que ninguém enxerga.
Nesse ponto, talvez você não tenha uma lista de estratégias.
Talvez tenha um workflow que merece ser modelado de forma mais explícita.
Lista também pode virar Map quando a escolha é por chave
Às vezes o serviço não precisa executar todos.
Ele precisa escolher um com base em uma chave.
Esse foi um ponto do post sobre @Qualifier: quando a escolha é dinâmica, ela pertence ao fluxo de execução.
Nesses casos, uma lista pode ser usada para montar um resolvedor:
public interface CalculadoraDesconto {
TipoCliente tipoCliente();
BigDecimal calcular(Pedido pedido);
}
@Component
public class CalculadoraDescontoResolver {
private final Map<TipoCliente, CalculadoraDesconto> calculadoras;
public CalculadoraDescontoResolver(List<CalculadoraDesconto> calculadoras) {
this.calculadoras = calculadoras.stream()
.collect(Collectors.toMap(
CalculadoraDesconto::tipoCliente,
Function.identity()
));
}
public CalculadoraDesconto resolver(TipoCliente tipoCliente) {
CalculadoraDesconto calculadora = calculadoras.get(tipoCliente);
if (calculadora == null) {
throw new IllegalArgumentException(
"Tipo de cliente não suportado: " + tipoCliente
);
}
return calculadora;
}
}
Aqui a lista é detalhe de composição interna.
O caso de uso não precisa conhecer a coleção.
Ele depende do resolvedor:
@Service
public class AplicarDescontoService {
private final CalculadoraDescontoResolver resolver;
public AplicarDescontoService(CalculadoraDescontoResolver resolver) {
this.resolver = resolver;
}
public BigDecimal aplicar(Pedido pedido) {
CalculadoraDesconto calculadora =
resolver.resolver(pedido.tipoCliente());
return calculadora.calcular(pedido);
}
}
Esse desenho deixa a escolha em um lugar claro.
O serviço não escolhe por posição.
Não compara nome de classe.
Não usa instanceof.
Ele delega a resolução para um componente que existe justamente para isso.
O risco é esconder acoplamento dentro da coleção
O fato de o Spring permitir injetar listas não significa que todo conjunto de Beans forma uma boa abstração.
Alguns sinais de alerta:
- o serviço pega sempre
get(0); - a ordem importa, mas não está declarada;
- uma implementação depende de efeito colateral de outra;
- cada item da lista exige um
ifdiferente; - a interface é genérica demais para o conjunto;
- adicionar um novo
Beanmuda comportamento global sem revisão.
Quando isso acontece, o problema não é o Spring.
É a fronteira do modelo.
Uma lista de Beans deveria reduzir acoplamento entre o orquestrador e as implementações.
Mas ela pode fazer o contrário se virar um saco de objetos parecidos apenas no tipo.
Se cada implementação precisa ser tratada de forma especial, talvez elas não pertençam ao mesmo contrato.
Se a ordem é cheia de dependências escondidas, talvez exista um processo explícito faltando.
Se a escolha é por chave, talvez falte um resolvedor.
O container entrega os objetos.
Ele não valida sozinho se aquele conjunto faz sentido como desenho.
O ponto que vale fixar
Injetar List<Bean> no Spring não é gambiarra.
É uma forma legítima de dizer que um componente trabalha com vários colaboradores do mesmo contrato.
Ela faz sentido quando o comportamento é coletivo, extensível e claro.
Validadores, pipelines, enriquecedores, handlers e políticas são bons candidatos.
Mas a lista não deve esconder uma escolha que deveria ser explícita.
Se você precisa de um único Bean, diga qual.
Se precisa escolher em tempo de execução, modele a seleção.
Se precisa de todos, a lista pode ser exatamente o contrato certo.
O cuidado é simples:
não use List<T> para calar o container.
Use List<T> quando o modelo realmente pede um conjunto.
