Interfaces em Java OOP: contratos, desacoplamento e múltiplas capacidades

Capítulo 6

Tempo estimado de leitura: 8 minutos

+ Exercício

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

  • 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 default e static, 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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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.: Notificador em vez de NotificadorEmail)
  • O cliente evita new de 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.: cobrar retornando um Recibo).
  • 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.

Agora responda o exercício sobre o conteúdo:

Qual mudança melhor reduz o acoplamento em um serviço que envia notificações e permite trocar de e-mail para SMS sem alterar a lógica do serviço?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Ao depender de uma interface e receber a implementação por injeção (ex.: no construtor), o cliente conhece apenas o contrato. Assim, é possível trocar a implementação (e-mail/SMS) na montagem, sem modificar a lógica do serviço.

Próximo capitúlo

Classes abstratas em Java OOP: template de comportamento e reutilização controlada

Arrow Right Icon
Capa do Ebook gratuito Java Orientado a Objetos: Do Conceito ao Código com Padrões de Projeto Básicos
35%

Java Orientado a Objetos: Do Conceito ao Código com Padrões de Projeto Básicos

Novo curso

17 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.