Interfaces como contratos: o que são e por que reduzem acoplamento
Uma interface em Java define um contrato: um conjunto de métodos que um tipo promete oferecer. O código cliente passa a depender do contrato (interface) e não de uma classe concreta. Isso reduz acoplamento porque:
- o cliente conhece apenas o que pode ser feito (assinaturas), não como é feito (implementação);
- é possível trocar implementações (por exemplo, um provedor de pagamento) sem alterar o código cliente;
- facilita testes, pois uma implementação “fake” pode substituir a real.
Em termos práticos: quando você programa “contra interfaces”, você cria pontos de variação explícitos no design, permitindo substituição de componentes com impacto mínimo.
Quando uma interface é uma boa ideia
- Há múltiplas implementações possíveis (ex.: e-mail, SMS, push).
- Você quer isolar dependências externas (ex.: gateway de pagamento, API de terceiros).
- Você quer testabilidade (substituir por dublês em testes).
- Você quer compor capacidades (um objeto pode “ter” várias habilidades via múltiplas interfaces).
Implementando interfaces: sintaxe e regras essenciais
Uma classe implementa uma interface com implements. Ela deve fornecer implementações para todos os métodos abstratos do contrato.
public interface Notificador { void enviar(String destino, String mensagem);}public class NotificadorEmail implements Notificador { @Override public void enviar(String destino, String mensagem) { System.out.println("Enviando e-mail para " + destino + ": " + mensagem); }}Pontos importantes:
- Uma classe pode implementar várias interfaces:
class X implements A, B, C. - Interfaces podem conter métodos
defaultestatic, mas use com parcimônia: o foco é manter o contrato claro. - Interfaces devem ser pequenas e coesas; contratos “gigantes” aumentam acoplamento e dificultam reuso.
Programação voltada a interface: cliente depende do contrato
O ganho real aparece quando o cliente recebe/guarda uma referência do tipo interface, e não do tipo concreto.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Exemplo: serviço de notificações com troca de implementação
Passo 1: defina o contrato.
public interface Notificador { void enviar(String destino, String mensagem);}Passo 2: crie implementações alternativas.
public class NotificadorSms implements Notificador { @Override public void enviar(String destino, String mensagem) { System.out.println("Enviando SMS para " + destino + ": " + mensagem); }}public class NotificadorPush implements Notificador { @Override public void enviar(String destino, String mensagem) { System.out.println("Enviando PUSH para " + destino + ": " + mensagem); }}Passo 3: escreva o cliente dependendo apenas da interface.
public class CadastroUsuarioService { private final Notificador notificador; public CadastroUsuarioService(Notificador notificador) { this.notificador = notificador; } public void cadastrar(String email, String telefone) { // ... regras de cadastro notificador.enviar(email, "Bem-vindo!"); }}Passo 4: troque a implementação sem alterar o cliente.
Notificador notificador = new NotificadorEmail();CadastroUsuarioService service = new CadastroUsuarioService(notificador);service.cadastrar("ana@exemplo.com", "+55 11 99999-0000");// Troca para SMS, sem mexer em CadastroUsuarioServiceNotificador notificador = new NotificadorSms();CadastroUsuarioService service = new CadastroUsuarioService(notificador);service.cadastrar("ana@exemplo.com", "+55 11 99999-0000");Observe que o código do serviço não muda. Apenas a “montagem” (criação/injeção) muda.
Checklist rápido: você está programando contra interface?
- Campos e parâmetros usam o tipo da interface? (ex.:
Notificadorem vez deNotificadorEmail) - O cliente evita
newde implementações concretas dentro da lógica principal? - As decisões de qual implementação usar ficam na borda (configuração/montagem)?
Interfaces em serviços: pagamentos com substituição de gateway
Pagamentos são um exemplo clássico de ponto de variação: diferentes provedores, ambientes (sandbox/produção), regras de autenticação e formatos de requisição.
Passo a passo: extraindo um contrato de pagamento
Passo 1: identifique o que o cliente realmente precisa. Normalmente, “autorizar/capturar” ou “cobrar”. Vamos modelar um contrato simples:
import java.math.BigDecimal;public interface GatewayPagamento { Recibo cobrar(BigDecimal valor, String moeda, String descricao);}public record Recibo(String idTransacao, boolean aprovado, String mensagem) {}Passo 2: crie implementações.
import java.math.BigDecimal;public class GatewayPagamentoStripe implements GatewayPagamento { @Override public Recibo cobrar(BigDecimal valor, String moeda, String descricao) { // chamada a API do provedor (simulada) return new Recibo("tx_stripe_123", true, "Aprovado"); }}import java.math.BigDecimal;public class GatewayPagamentoPaypal implements GatewayPagamento { @Override public Recibo cobrar(BigDecimal valor, String moeda, String descricao) { // chamada a API do provedor (simulada) return new Recibo("tx_paypal_456", true, "Aprovado"); }}Passo 3: cliente depende do contrato.
import java.math.BigDecimal;public class CheckoutService { private final GatewayPagamento gateway; public CheckoutService(GatewayPagamento gateway) { this.gateway = gateway; } public Recibo finalizarCompra(BigDecimal total) { // ... validações e regras de negócio return gateway.cobrar(total, "BRL", "Compra na loja"); }}Passo 4: troca de implementação sem alterar o cliente.
CheckoutService checkout = new CheckoutService(new GatewayPagamentoStripe());Recibo recibo = checkout.finalizarCompra(new BigDecimal("199.90"));// Troca para PayPal sem mexer no CheckoutServiceCheckoutService checkout = new CheckoutService(new GatewayPagamentoPaypal());Recibo recibo = checkout.finalizarCompra(new BigDecimal("199.90"));Evite “vazar” detalhes do provedor para o contrato
Um erro comum é desenhar a interface com parâmetros específicos de um provedor (ex.: tokenStripe, payerIdPaypal). Isso acopla o cliente ao detalhe externo. Prefira um contrato com dados do domínio do seu sistema (valor, moeda, descrição) e deixe a adaptação para cada implementação.
Composição de capacidades: múltiplas interfaces para múltiplas habilidades
Uma classe pode ter várias “capacidades” implementando múltiplas interfaces. Isso permite compor comportamentos sem criar hierarquias rígidas.
Exemplo: notificações com rastreabilidade e agendamento
Em vez de uma interface única enorme, separe capacidades:
public interface Notificador { void enviar(String destino, String mensagem);}public interface Rastreavel { String obterIdUltimoEnvio();}public interface Agendavel { void agendar(String destino, String mensagem, long epochMillis);}Uma implementação pode oferecer várias capacidades:
public class NotificadorEmailAvancado implements Notificador, Rastreavel, Agendavel { private String ultimoId; @Override public void enviar(String destino, String mensagem) { this.ultimoId = "mail_" + System.currentTimeMillis(); System.out.println("E-mail enviado id=" + ultimoId); } @Override public String obterIdUltimoEnvio() { return ultimoId; } @Override public void agendar(String destino, String mensagem, long epochMillis) { System.out.println("E-mail agendado para " + epochMillis); }}O cliente escolhe a capacidade que precisa:
public class AlertaService { private final Notificador notificador; public AlertaService(Notificador notificador) { this.notificador = notificador; } public void alertar(String destino) { notificador.enviar(destino, "Alerta importante"); }}Outro cliente pode exigir rastreabilidade:
public class AuditoriaNotificacaoService { private final Notificador notificador; private final Rastreavel rastreavel; public AuditoriaNotificacaoService(Notificador notificador, Rastreavel rastreavel) { this.notificador = notificador; this.rastreavel = rastreavel; } public void enviarEAuditar(String destino, String msg) { notificador.enviar(destino, msg); System.out.println("Auditando envio id=" + rastreavel.obterIdUltimoEnvio()); }}Na montagem, você pode passar o mesmo objeto para ambos os parâmetros, desde que ele implemente as duas interfaces:
NotificadorEmailAvancado impl = new NotificadorEmailAvancado();AuditoriaNotificacaoService svc = new AuditoriaNotificacaoService(impl, impl);svc.enviarEAuditar("ana@exemplo.com", "Olá!");Regra prática: prefira interfaces pequenas e combináveis
- Se um cliente só precisa de
enviar, ele não deveria ser obrigado a depender de métodos de agendamento. - Separar capacidades reduz o impacto de mudanças e melhora reuso.
Extraindo interfaces: encontrando pontos de variação
Interfaces são mais úteis quando representam um ponto do sistema que pode variar. Para descobrir esses pontos, procure por:
- Condicionais por tipo (ex.:
if (tipo == EMAIL) ... else if ...) - Dependência direta de bibliotecas externas dentro do núcleo do sistema
- Regras de negócio misturadas com I/O (rede, disco, console)
- Trocas frequentes de fornecedor/estratégia (ex.: meios de pagamento, antifraude, frete)
Atividade 1: substituição de implementações sem alterar o cliente
Considere o código abaixo (acoplado):
public class CheckoutService { public void finalizarCompra() { NotificadorEmail notificador = new NotificadorEmail(); notificador.enviar("ana@exemplo.com", "Compra aprovada"); }}- Identifique o problema de acoplamento.
- Extraia uma interface adequada.
- Altere o serviço para depender da interface.
- Crie uma segunda implementação (ex.: SMS) e demonstre a troca sem alterar
CheckoutService.
Atividade 2: identificando pontos de variação em pagamentos
Você recebeu o seguinte requisito: “o sistema deve suportar Stripe e PayPal agora, e possivelmente outros gateways depois”.
- Liste quais partes variam entre provedores (autenticação, endpoint, formato de requisição, id de transação).
- Defina um contrato mínimo que o domínio precisa (ex.:
cobrarretornando umRecibo). - Implemente duas classes concretas e um cliente (
CheckoutService) que não conhece detalhes do provedor.
Atividade 3: decompondo uma interface “gorda” em capacidades
Uma interface foi criada assim:
public interface NotificacaoCompleta { void enviar(String destino, String mensagem); void agendar(String destino, String mensagem, long epochMillis); void cancelar(String idAgendamento); String obterIdUltimoEnvio();}- Separe em interfaces menores por responsabilidade/capacidade.
- Mostre um cliente que depende apenas de
enviar. - Mostre outro cliente que depende apenas de rastreabilidade.
Armadilhas comuns e como evitá-las
Interface baseada em implementação (contrato “vazando” detalhes)
- Sinal: métodos com nomes/parametrizações específicas de um fornecedor.
- Correção: redesenhe o contrato em termos do que o domínio precisa; adapte detalhes no implementador.
Interface grande demais
- Sinal: muitos métodos não usados por vários clientes.
- Correção: aplique segregação de interfaces: contratos menores e combináveis.
Cliente instanciando implementações diretamente
- Sinal:
new ImplementacaoConcreta()dentro do serviço principal. - Correção: receba a dependência por parâmetro (construtor) e faça a escolha na montagem.