Acoplamento e Contratos

Aula 5 — Programação Orientada a Objetos

Autor

Marcos M. Raimundo — Instituto de Computação, UNICAMP

Data de Publicação

30 de agosto de 2026

Slides Lista de aulas

1 Proposta da Aula

Na Aula 4, aprendemos a conectar objetos via Associação e a rotear tarefas via Delegação. É tentador achar que, tendo separado o código em classes e passado as dependências pelo construtor, o sistema já está desacoplado. Não está — o acoplamento físico (como as classes se instanciam) é só metade do problema. Uma classe pode receber a referência de um colaborador “corretamente” e, ainda assim, invadir a caixa preta dele por dentro.

O roteiro, em quatro perguntas:

  1. Como uma associação “estruturalmente correta” pode esconder uma violação de encapsulamento?
  2. Como medir o acoplamento com números, em vez de “sentir” que o código está ruim?
  3. Quem deve ser responsável por uma tarefa, quando várias classes têm acesso aos dados envolvidos?
  4. O que fazer quando até a delegação bem-feita ainda deixa o núcleo do sistema refém de uma classe concreta?

2 O Falso Desacoplamento

public class Pedido {
    private Carrinho carrinho;

    public Pedido(Carrinho carrinho) {
        this.carrinho = carrinho; // Associacao estrutural correta
    }

    // VIOLACAO LOGICA: o Pedido invade a "caixa preta" do Carrinho
    public double calcularCustoTotal() {
        double total = 0.0;
        // Feature Envy: o Pedido inveja os dados do Carrinho
        for (Produto p : carrinho.getItens()) {
            total += p.precoFinal();
        }
        return total;
    }
}

A associação via construtor é estruturalmente correta. O erro está em calcularCustoTotal(): em vez de perguntar ao Carrinho “qual é o seu total?”, o Pedido extrai a lista inteira e faz a conta por fora. Isso é Feature Envy (Inveja de Recursos) — um método mais interessado nos dados de um colaborador do que nos seus próprios. O sintoma é a obsessão por getters; o diagnóstico definitivo é notar que qualquer mudança na estrutura interna do Carrinho (trocar List por Map, por exemplo) forçaria mudanças em Pedido, mesmo os dois estando em arquivos separados.

A cura é Move Method: a lógica de soma pertence a quem tem os dados.

public class Pedido {
    private Carrinho carrinho;
    public Pedido(Carrinho carrinho) { this.carrinho = carrinho; }

    // REFATORACAO: delegacao pura, o "como" e responsabilidade do Carrinho
    public double calcularCustoTotal() {
        return this.carrinho.getTotal();
    }
}

O Pedido não sabe mais se o Carrinho itera um ArrayList, consulta em tempo real, ou mantém um saldo já calculado — voltou a ser uma caixa preta de verdade. Sem essa correção, se o marketing decidir uma promoção “Compre 3, Leve 4”, e a lógica de soma estiver espalhada em Pedido, Fatura, Checkout e RelatorioFinanceiro, a mudança precisa ser caçada e repetida em todos eles — a Cirurgia de Espingarda já vista na Aula 4, agora com uma causa raiz mais específica: violação de Tell, Don’t Ask.

3 Medindo o Acoplamento: a Métrica CBO

“Sentir” que o código está acoplado demais não basta num ambiente profissional. A Suíte CK (Chidamber & Kemerer) define o CBO (Coupling Between Object Classes): conta a quantas classes externas uma classe está ligada, por métodos, atributos ou parâmetros.

// Cenario A: Acoplamento Invasivo
public class Pedido {
    private Carrinho carrinho;                    // Dependencia 1: Carrinho
    public double calcularCustoTotal() {
        double total = 0.0;
        for (Produto p : carrinho.getItens()) {   // Dependencia 2: Produto
            total += p.precoFinal();
        }
        return total;
    }
}
// CBO(Pedido) = 2

// Cenario B: Delegacao Pura
public class Pedido {
    private Carrinho carrinho;                     // Dependencia 1: Carrinho
    public double calcularCustoTotal() {
        return this.carrinho.getTotal();           // Pedido so conhece o Carrinho
    }
}
// CBO(Pedido) = 1

A diferença parece pequena num exemplo isolado, mas em sistemas com centenas de classes, manter o CBO baixo é o que minimiza a necessidade de testes extensivos e o risco de Efeito Cascata. A regra de ouro não é buscar CBO zero (um sistema sem conexão nenhuma não faz nada) — é manter cada conexão o mais fraca possível, o assunto do próximo bloco.

DicaFeature Envy e CBO — Se o CBO de uma classe é baixo, isso já garante que o acoplamento dela é saudável?

Escreva sua resposta e compare com um colega antes de avançar (2 min). Depois julgue os 4 itens de V/F abaixo — a questão só conta se acertar os 4:

  • □ Passar uma dependência via construtor garante, por si só, o desacoplamento lógico entre as classes.
  • □ Feature Envy é o sintoma de um método mais interessado nos dados de outra classe do que nos seus próprios.
  • □ Refatorar de Inveja de Recursos para Delegação Pura reduz o CBO da classe orquestradora.
  • □ Um CBO baixo, isoladamente, já garante que a qualidade de cada dependência restante é saudável.

4 A Escala de Myers: Qualidade da Dependência

O CBO conta dependências; a Escala de Myers classifica sua toxicidade, do pior para o ideal:

1. Acoplamento de Conteúdo (o pecado original) — uma classe altera diretamente os atributos internos de outra:

public class Carrinho { public List itens = new ArrayList<>(); } // ATRIBUTO PUBLICO!
public class Pedido {
    public void aplicarDescontoManual(Carrinho c) {
        c.itens.clear(); // VIOLACAO: altera a estrutura do Carrinho por fora
    }
}

É a morte do encapsulamento — o objeto vira um aglomerado de variáveis globais disfarçadas.

2. Acoplamento Comum (global) — dependência de estado global ou de métodos estáticos:

public class Pedido {
    public void verificarPagamento() {
        // Dependencia estatica: impossivel testar sem disparar o codigo real
        Notificacao.enviarEmailConfirmacao(this.cliente, this.rastreio);
    }
}

O acoplamento fica “enterrado” no corpo do método, invisível na assinatura — destrói a testabilidade e viola o Princípio Aberto/Fechado (mudar de e-mail para WhatsApp exige mexer em Pedido).

3. Acoplamento de Estampa — passar um objeto inteiro quando só uma fração dele é usada:

public class NotificacaoEmail {
    public void enviarConfirmacao(Cliente cliente) {
        String email = cliente.getEmail(); // so precisa disso, mas "conhece" o Cliente inteiro
        System.out.println("Enviando para: " + email);
    }
}

Renomear Cliente para Usuario quebra NotificacaoEmail, mesmo que a lógica de e-mail não tenha mudado nada.

4. Acoplamento de Dados (o ideal) — só o dado estritamente necessário trafega:

public class Notificacao {
    // Desconhece completamente a existencia de "Cliente"
    public static void enviarEmailConfirmacao(String emailDestino, String rastreio) {
        System.out.println("Enviando e-mail para: " + emailDestino);
    }
}
// O orquestrador extrai o dado antes de chamar:
Notificacao.enviarEmailConfirmacao(this.cliente.getEmail(), this.rastreio);

Com essa refatoração, Notificacao vira um serviço reutilizável — pode notificar clientes, fornecedores, administradores, sem carregar toda a bagagem de Cliente junto.

DicaA Escala de Myers — Por que o Acoplamento de Estampa é considerado “médio” — pior que Dados, mas melhor que Comum ou Conteúdo?

Escreva sua resposta e compare com um colega antes de avançar (2 min). Depois julgue os 4 itens de V/F abaixo — a questão só conta se acertar os 4:

  • □ O Acoplamento de Conteúdo, o pior nível, ocorre quando uma classe modifica diretamente atributos de outra.
  • □ O Acoplamento Comum via métodos estáticos é fácil de testar, pois não exige instanciar objetos.
  • □ O Acoplamento de Estampa cria dependências transitivas: quem recebe o objeto herda tudo que ele conhece.
  • □ O Acoplamento de Dados é o nível ideal, pois módulos comunicam-se só com o estritamente necessário.

5 GRASP: o Especialista na Informação

Para além do diagnóstico intuitivo de code smells, o framework GRASP (Craig Larman) formaliza um critério: a responsabilidade de uma tarefa deve pertencer à classe que possui a maior parte das informações necessárias para cumpri-la — o Especialista na Informação.

“Calcular o custo total” exige conhecer a lista de itens, quantidades e preços — informação que mora inteiramente dentro de Carrinho. A teoria de Larman classifica Carrinho como o Especialista; tentar fazer esse cálculo em Pedido força um roubo de informação (Feature Envy). Respeitar o Especialista na Informação garante que o comportamento siga os dados — resolvendo, ao mesmo tempo, o problema técnico (Feature Envy) e o organizacional (lógica de negócio centralizada e testável).

6 O Limite da Composição e o Muro de Fronteira (DIP)

Mesmo com Feature Envy resolvido, ainda existe uma falha estrutural: acoplamento a classes concretas.

public class Cliente {
    private Cartao cartao; // acoplamento forte com implementacao CONCRETA
    public void cadastrarCartao(double limite) { this.cartao = new Cartao(limite); }
    public void realizarPagamento(double valor) { this.cartao.processar(valor); }
}

Cliente (módulo de alto nível, regra de negócio) depende fisicamente de Cartao (módulo de baixo nível, detalhe técnico). Se o negócio pedir Pix e Boleto, o resultado é uma “gambiarra”:

public class Cliente {
    private Cartao cartao;
    private Pix pix;
    private Boleto boleto;

    public void pagar(double valor) {
        // Violacao do OCP: complexidade ciclomatica explode
        if (this.cartao != null) this.cartao.processar(valor);
        else if (this.pix != null) this.pix.transferir(valor);
        else if (this.boleto != null) this.boleto.gerarCodigoBarra(valor);
    }
}

Cada novo método de pagamento exige reabrir e recompilar Cliente — violando o Princípio Aberto/Fechado (aberto para extensão, fechado para modificação).

A solução é o Princípio da Inversão de Dependência (DIP, Robert C. Martin): módulos de alto nível não devem depender de módulos de baixo nível — ambos devem depender de abstrações.

public class Cliente {
    // Dependemos de uma abstracao! O "Muro de Fronteira" esta erguido.
    private Pagavel formaDePagamento;

    public void pagar(double valor) {
        // Polimorfismo: o Cliente confia cegamente no contrato
        this.formaDePagamento.pagamentoConfirmado(valor);
    }
}

A dependência se inverte: Cliente já não olha para baixo pedindo serviços a Cartao — ele impõe um contrato (Pagavel), e são Cartao e Pix que precisam olhar para cima e obedecer. Se surgir o pagamento por criptomoeda amanhã, Cliente permanece intocado.

O gancho para a próxima aula. Isso levanta um problema técnico: o Java é fortemente tipado — como o compilador aceita que a mesma variável formaDePagamento receba ora um Pix, ora um Cartao, se são classes completamente diferentes? A resposta é o Polimorfismo, viabilizado por Interfaces e Herança — os mecanismos de linguagem que a Aula 6 vai construir.

DicaInversão de Dependência — O que exatamente “se inverteu” no Princípio da Inversão de Dependência?

O que dependia de quê antes, e o que passa a depender de quê depois? Escreva sua resposta e compare com um colega antes de avançar (2 min). Depois julgue os 4 itens de V/F abaixo — a questão só conta se acertar os 4:

  • □ No DIP, tanto o módulo de alto nível quanto o de baixo nível devem depender de uma abstração comum.
  • □ Antes do DIP, Cliente (alto nível) dependia diretamente de Cartao (baixo nível).
  • □ Depois do DIP, é Cliente que precisa se adaptar às mudanças em Cartao e Pix.
  • □ A cadeia de if/else para escolher entre Cartao, Pix e Boleto é um sintoma de violação do Princípio Aberto/Fechado.

7 Conclusão

Voltando às quatro perguntas da abertura:

  1. Associação correta, comportamento errado: passar a dependência pelo construtor não impede o objeto de “espreitar” o interior do colaborador — isso ainda é Feature Envy.
  2. Medir com números: CBO conta quantas dependências existem; a Escala de Myers classifica quão tóxica cada uma é.
  3. Quem deve fazer o quê: o Especialista na Informação — a classe que já tem os dados necessários.
  4. Além da delegação: quando até a delegação bem-feita ainda acopla o núcleo a uma classe concreta, a saída é inverter a dependência via uma abstração (DIP).

Ponte para a Aula 6

Terminamos com uma pergunta em aberto: como o compilador permite que a mesma variável aceite tipos completamente diferentes, desde que obedeçam ao mesmo contrato? A Aula 6 responde com os mecanismos de linguagem que tornam isso possível — Interfaces e Herança — e com testes automatizados (JUnit) para verificar que esses contratos são cumpridos.

Exercícios Soluções