Spring não resolve acoplamento

Spring ajuda a reduzir um tipo importante de acoplamento.

Mas ele não elimina acoplamento por decreto.

Essa confusão aparece quando o time troca new por injeção de dependência e conclui que o desenho ficou automaticamente desacoplado.

Não ficou.

Ficou gerenciado pelo container.

Isso é útil, mas não é a mesma coisa.

O Spring pode criar objetos, conectar dependências, aplicar proxies, organizar configuração e controlar ciclo de vida.

Mas ele não decide sozinho se suas classes dependem das coisas certas.

Essa decisão continua sendo de arquitetura.


O acoplamento que o Spring realmente ajuda a remover

Sem Spring, é comum encontrar código assim:

public class FecharPedidoService {

    private final PedidoRepository pedidoRepository = new PedidoRepository();
    private final EmailGateway emailGateway = new EmailGateway();

    public void fechar(Long pedidoId) {
        Pedido pedido = pedidoRepository.buscarPorId(pedidoId);
        pedido.fechar();
        emailGateway.enviarConfirmacao(pedido);
    }
}

Aqui a classe não só usa colaboradores.

Ela também decide como eles são criados.

Isso mistura regra de aplicação com montagem de objeto.

Quando o Spring entra, esse controle muda de lugar:

@Service
public class FecharPedidoService {

    private final PedidoRepository pedidoRepository;
    private final EmailGateway emailGateway;

    public FecharPedidoService(
        PedidoRepository pedidoRepository,
        EmailGateway emailGateway
    ) {
        this.pedidoRepository = pedidoRepository;
        this.emailGateway = emailGateway;
    }

    public void fechar(Long pedidoId) {
        Pedido pedido = pedidoRepository.buscarPorId(pedidoId);
        pedido.fechar();
        emailGateway.enviarConfirmacao(pedido);
    }
}

Agora a classe declara do que precisa, e o container entrega as dependências.

Esse é o ganho real.

O código deixou de controlar a criação dos colaboradores.

Esse ponto conversa diretamente com os posts sobre IoC, DI e @Autowired.

Mas repare no limite:

a classe ainda depende de PedidoRepository e EmailGateway.

O Spring não apagou essas dependências.

Ele só tornou a montagem delas externa à classe.


Dependência explícita não é dependência correta

Constructor injection deixa dependência visível.

Isso é bom.

Mas visibilidade não garante bom desenho.

Uma classe pode declarar dependências com clareza e ainda assim depender de coisas demais, de coisas erradas ou de detalhes que não deveriam chegar até ela.

Veja este exemplo:

@Service
public class AprovarPagamentoService {

    private final PedidoRepository pedidoRepository;
    private final ClienteRepository clienteRepository;
    private final CupomRepository cupomRepository;
    private final EstoqueRepository estoqueRepository;
    private final EmailGateway emailGateway;
    private final ErpClient erpClient;
    private final ObjectMapper objectMapper;

    public AprovarPagamentoService(
        PedidoRepository pedidoRepository,
        ClienteRepository clienteRepository,
        CupomRepository cupomRepository,
        EstoqueRepository estoqueRepository,
        EmailGateway emailGateway,
        ErpClient erpClient,
        ObjectMapper objectMapper
    ) {
        this.pedidoRepository = pedidoRepository;
        this.clienteRepository = clienteRepository;
        this.cupomRepository = cupomRepository;
        this.estoqueRepository = estoqueRepository;
        this.emailGateway = emailGateway;
        this.erpClient = erpClient;
        this.objectMapper = objectMapper;
    }
}

Não existe new.

Não existe field injection.

O Spring consegue montar tudo.

Mesmo assim, o cheiro de problema está ali.

Essa classe parece conhecer persistência, estoque, cupom, cliente, integração externa, serialização e comunicação.

O container resolveu a montagem.

Ele não resolveu a falta de fronteira.

Quando uma classe precisa de colaboradores demais, isso normalmente indica que ela está segurando uma responsabilidade grande demais ou coordenando conceitos que deveriam estar melhor separados.

Trocar new por construtor melhora a honestidade do código.

Não melhora automaticamente o desenho.


Interface também pode acoplar

Outro engano comum é acreditar que criar uma interface entre tudo resolve acoplamento.

Às vezes ajuda.

Às vezes só cria uma camada a mais com o mesmo problema.

public interface PedidoRepositoryService {
    PedidoEntity findById(Long id);
    PedidoEntity save(PedidoEntity pedido);
    void deleteById(Long id);
}

Esse contrato não protege a aplicação de detalhe técnico.

Ele vaza Entity, fala a língua do repositório e replica operações de persistência.

A classe que depende dessa interface continua presa ao modelo de banco.

Só que agora existe um nome mais abstrato no caminho.

Uma interface faz sentido quando representa uma capacidade do domínio ou uma porta da aplicação:

public interface Pedidos {
    Pedido buscarPedidoAberto(PedidoId pedidoId);
    void salvar(Pedido pedido);
}

Aqui o contrato fala a língua do caso de uso.

Ele ainda pode ser implementado com JPA, JDBC, HTTP, arquivo ou qualquer outra tecnologia.

Mas o código de aplicação não precisa falar nesses termos.

A abstração não nasce da palavra interface.

Nasce de uma fronteira bem escolhida.


Anotação de Spring também é acoplamento

Spring aparece no código através de anotações, tipos, configurações e convenções.

Isso não é necessariamente um problema.

Em uma aplicação Spring, é normal ter classes de entrada, configuração, controllers, repositories e adapters conhecendo o framework.

O problema é quando esse acoplamento escorre para lugares que deveriam expressar regra de negócio limpa.

Por exemplo:

@Entity
@Service
public class Pedido {

    @Autowired
    private PedidoRepository pedidoRepository;

    public void cancelar() {
        this.status = StatusPedido.CANCELADO;
        pedidoRepository.save(this);
    }
}

Esse exemplo concentra vários problemas de uma vez.

A entidade conhece Spring, conhece persistência e decide salvar a si mesma.

O resultado é um modelo difícil de testar, difícil de mover e difícil de entender fora do runtime do framework.

O desenho mais saudável costuma separar comportamento do domínio de infraestrutura:

public class Pedido {

    private StatusPedido status;

    public void cancelar() {
        this.status = StatusPedido.CANCELADO;
    }
}

@Service
public class CancelarPedidoService {

    private final PedidoRepository pedidoRepository;

    public CancelarPedidoService(PedidoRepository pedidoRepository) {
        this.pedidoRepository = pedidoRepository;
    }

    @Transactional
    public void cancelar(Long pedidoId) {
        Pedido pedido = pedidoRepository.buscarPorId(pedidoId);
        pedido.cancelar();
    }
}

Agora a entidade expressa comportamento.

O service coordena o caso de uso.

O repositório cuida da persistência.

O Spring participa da aplicação, mas não precisa invadir todos os conceitos.

Esse ponto também se conecta com o post sobre nem todo @Service deveria existir: o objetivo não é criar camadas por cerimônia, e sim colocar responsabilidades nos lugares certos.


O que o Spring entrega e o que ele não entrega

Spring entrega uma infraestrutura poderosa para montar a aplicação.

Ele ajuda com:

  • criação e ciclo de vida de objetos;
  • injeção de dependências;
  • configuração por ambiente;
  • transações;
  • proxies;
  • integração com web, dados, mensageria e segurança.

Mas ele não entrega automaticamente:

  • coesão;
  • fronteiras de módulo;
  • bons contratos;
  • casos de uso pequenos;
  • entidades bem modeladas;
  • separação entre domínio e infraestrutura.

Essas coisas não aparecem porque a classe recebeu @Service.

Também não aparecem porque a dependência veio pelo construtor.

Elas aparecem quando o desenho da aplicação força cada parte a depender apenas do que faz sentido para sua responsabilidade.


O ponto que vale fixar

Spring reduz o acoplamento de criação.

Ele não resolve o acoplamento conceitual da aplicação.

Se uma classe depende de detalhes demais, sabe coisas demais ou conversa com camadas erradas, o container só vai montar esse problema com eficiência.

Injeção de dependência deixa as relações visíveis.

Depois disso, ainda cabe a você decidir se essas relações deveriam existir.