Acoplamento e Contratos
Aula 5 — Programação Orientada a Objetos
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:
- Como uma associação “estruturalmente correta” pode esconder uma violação de encapsulamento?
- Como medir o acoplamento com números, em vez de “sentir” que o código está ruim?
- Quem deve ser responsável por uma tarefa, quando várias classes têm acesso aos dados envolvidos?
- 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) = 1A 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.
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.
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.
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 deCartao(baixo nível). - □ Depois do DIP, é
Clienteque precisa se adaptar às mudanças emCartaoePix. - □ A cadeia de
if/elsepara escolher entreCartao,PixeBoletoé um sintoma de violação do Princípio Aberto/Fechado.
7 Conclusão
Voltando às quatro perguntas da abertura:
- 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.
- Medir com números: CBO conta quantas dependências existem; a Escala de Myers classifica quão tóxica cada uma é.
- Quem deve fazer o quê: o Especialista na Informação — a classe que já tem os dados necessários.
- 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.