MÓDULO 00 · CONCEITO 02 DE 5

Coesão & Acoplamento

A métrica real de qualidade estrutural — o que torna um código possível de evoluir, ou não.

Tempo de leitura ~22 min Pré-requisito Clean Code & SOLID Próximo Composição vs Herança

Se SOLID é o vocabulário do design orientado a objetos, coesão e acoplamento são a gramática. Os cinco princípios de SOLID podem ser todos derivados desses dois conceitos mais primitivos — e, em boa medida, foram. A formulação de Larry Constantine e Edward Yourdon, em Structured Design (1979), antecede o design orientado a objetos por mais de uma década e continua sendo a referência mais clara que existe sobre o que é "código bem estruturado". Toda discussão moderna sobre arquitetura — hexagonal, microsserviços, monolito modular, DDD tático — é, em última instância, uma discussão sobre como aumentar coesão e reduzir acoplamento em escalas diferentes.

O que torna esses dois conceitos especialmente úteis é que eles são medidas de propriedades estruturais do código, não de gosto pessoal. "Código limpo" depende do leitor; coesão e acoplamento, com algum trabalho, podem ser quantificados. Mais importante na prática diária: eles dão linguagem para explicar por que um código parece bom ou ruim. Quando um colega diz "esse módulo está bagunçado", o que ele está sentindo quase sempre é alguma combinação de baixa coesão (o módulo faz coisas demais e desconexas) ou alto acoplamento (qualquer mudança aqui obriga a tocar em três outros módulos). Nomear o problema é o primeiro passo para resolvê-lo.

Coesão — o quanto um módulo é "uma coisa só"

Coesão mede o grau em que os elementos dentro de um mesmo módulo pertencem juntos. Um módulo altamente coeso tem todas as suas partes contribuindo para um único propósito bem definido; um módulo de baixa coesão é uma colcha de retalhos onde funções vagamente relacionadas convivem por acidente histórico ou organizacional. A consequência prática é direta: módulos coesos mudam por uma razão e mudam num só lugar. Módulos de baixa coesão mudam por várias razões e espalham mudança pelo sistema.

Constantine identificou sete níveis de coesão, do pior ao melhor — do mais acidental ao mais funcional. Não é necessário decorar a taxonomia, mas vale conhecer os extremos porque eles aparecem em código real com nomes que enganam.

Os tipos ruins de coesão (que aparecem disfarçados)

Coesão coincidental é o pior caso: elementos juntos sem nenhum relacionamento real. Um arquivo chamado Utils.cs ou helpers.py tipicamente sofre disso — vira o cesto onde se joga qualquer função que não tem casa em outro lugar. Sintoma: alguém pergunta "onde coloco essa função?", e a resposta padrão é "joga no Utils". Esse arquivo cresce indefinidamente e, em seis meses, ninguém sabe mais o que tem dentro.

Coesão lógica agrupa coisas porque elas parecem da mesma categoria, mesmo que não compartilhem nada operacionalmente. O exemplo clássico é uma classe InputHandler que tem handleKeyboard(), handleMouse() e handleNetwork() — três coisas conceitualmente "input", mas que não compartilham estado, ciclo de vida ou razão para mudar. Frequentemente vem com um parâmetro de tipo: handle(InputType type, byte[] data), e dentro um switch escolhendo o que fazer. Isso é tanto baixa coesão quanto violação clara de OCP.

Coesão temporal agrupa elementos porque eles acontecem ao mesmo tempo, mesmo sem relação lógica. O caso óbvio é a função initialize() que faz vinte coisas ao subir a aplicação: configura logging, conecta no banco, registra handlers HTTP, carrega cache, valida licenças. Cada uma dessas coisas muda por razões completamente diferentes; estarem na mesma função é circunstância, não design.

Coesão funcional — o objetivo

O nível mais alto na taxonomia é a coesão funcional: todos os elementos do módulo contribuem para uma única tarefa bem definida. Um exemplo simples é um módulo PasswordHasher que tem hash(password) e verify(password, hash). Os dois métodos compartilham invariantes (mesmo algoritmo, mesma forma de salt, mesmo trabalho criptográfico), e mudar um sem o outro provavelmente quebra o sistema. Eles pertencem juntos. Esse é o tipo de coesão que se busca.

Note como a métrica é "razão para mudar" — exatamente o critério usado pelo SRP. Não é coincidência: o Single Responsibility Principle é, na prática, uma reformulação operacional da busca por coesão funcional. Um módulo com uma única razão para mudar é, por construção, funcionalmente coeso.

armadilha comum

Existe a tentação de medir coesão por linhas de código — "essa classe é grande, deve ser pouco coesa". Tamanho não é coesão. Uma classe de quatrocentas linhas pode ser perfeitamente coesa se todas as quatrocentas servem ao mesmo propósito; uma classe de vinte linhas pode ser anti-coesa se faz três coisas não-relacionadas. Tamanho importa para legibilidade. Coesão é sobre propósito.

Acoplamento — o quanto um módulo depende dos outros

Acoplamento mede a interdependência entre módulos. Acoplamento alto significa que mudanças em um módulo se propagam para outros; acoplamento baixo significa que você pode trocar a implementação interna de um módulo sem afetar quem o usa. Constantine identificou também sete níveis de acoplamento, do pior ao melhor — e aqui também os nomes ajudam a diagnosticar problemas reais.

Os tipos ruins de acoplamento

Acoplamento de conteúdo é o pior: um módulo lê ou modifica diretamente os internals de outro. Em linguagens permissivas, isso aparece como acessar campos privados via reflection, ou — pior — manipular bytes diretamente em memória compartilhada. Em código mais comum, aparece como acessar variáveis privadas através de "amigos" (friend class em C++, internal usado de forma promíscua em C#, módulos que importam _implementation_details em Python).

Acoplamento comum ocorre quando módulos compartilham uma variável global mutável. O sintoma clássico em código antigo é um Singleton que guarda estado global — qualquer módulo pode mudá-lo, e qualquer módulo é afetado. Em sistemas web modernos, o equivalente são variáveis de ambiente lidas em vinte lugares diferentes do código (em vez de injetadas via configuração) ou um cache global compartilhado sem disciplina.

Acoplamento de controle aparece quando um módulo passa um parâmetro que diz ao outro o que fazer. A função processOrder(order, sendEmail=true, saveToDb=true, logEvent=false) é um exemplo: o chamador conhece detalhes de implementação do callee e os controla. O sintoma é o boolean trap — funções com várias flags booleanas que, no call site, viram processOrder(order, true, false, true), ilegível.

Acoplamento de dados estampados (stamp coupling) é mais sutil: um módulo recebe uma estrutura grande, mas usa apenas alguns campos. Isso obriga o caller a conhecer a estrutura inteira, e mudanças em campos não-usados ainda assim quebram a interface. Em microsserviços, isso é a praga dos contratos gigantescos passados entre serviços que só precisam de meia dúzia de campos.

Acoplamento de dados — o objetivo

O melhor caso é o acoplamento de dados: um módulo passa para o outro apenas os dados que esse outro precisa, em parâmetros simples e bem definidos. calculateShipping(weight, distance) é assim — não calculateShipping(order), onde o método precisa saber o que é order e como navegar na estrutura. A diferença parece pequena em código simples, mas faz diferença enorme em sistemas grandes: módulos com acoplamento de dados têm contratos pequenos e estáveis.

O efeito combinado — a única matriz que importa

Coesão e acoplamento sempre andam juntos, e o objetivo é o quadrante "alta coesão, baixo acoplamento". Os outros três quadrantes têm patologias específicas que vale reconhecer:

heurística do sênior

Quando estiver decidindo onde colocar uma função nova, faça duas perguntas: "as outras funções do módulo onde vou colocar mudam pelas mesmas razões que essa?" (coesão) e "essa função vai precisar saber detalhes internos de outros módulos para fazer seu trabalho?" (acoplamento). Se as respostas forem "sim" e "não", está no lugar certo. Caso contrário, vale considerar outra casa.

Acoplamento aferente vs eferente

Robert Martin introduziu duas métricas operacionais que valem internalizar. Acoplamento aferente (Ca) é o número de outros módulos que dependem deste; acoplamento eferente (Ce) é o número de módulos dos quais este depende. Os dois números têm leituras diferentes:

Um módulo com Ca alto e Ce baixo é estável: muitos dependem dele, ele depende de poucos. Mudá-lo é caro, porque a mudança se propaga para todos os dependentes. Esses módulos devem ser abstrações estáveis — interfaces, contratos, tipos de domínio. Em geral, devem ser deixados em paz.

Um módulo com Ca baixo e Ce alto é instável: poucos dependem dele, ele depende de muitos. É barato mudá-lo, porque pouca coisa quebra junto. Esses módulos costumam ser implementações concretas, adaptadores, código de aplicação que orquestra. É exatamente onde você quer concentrar a maior parte da volatilidade do sistema.

A patologia é o módulo com Ca alto e Ce alto: muitos dependem dele e ele depende de muitos. Cada mudança no que ele depende força mudanças nele, e cada mudança nele dispara mudanças nos dependentes. É um amplificador de mudança — e tipicamente é o "deus de classe" da codebase, aquele arquivo onde todo mundo acaba mexendo. Identificá-lo e dividi-lo é geralmente o primeiro grande refactoring que vale a pena num sistema legado.

O mesmo conceito em três linguagens

Cada uma das três linguagens da formação tem ferramentas próprias para gerenciar acoplamento. Veja como o mesmo problema — separar a regra de negócio do envio de e-mail — é resolvido idiomaticamente em cada uma:

C#
// Interface no domínio (estável, Ca alto)
public interface INotifier {
    Task Notify(string to, string msg);
}

// Serviço depende da abstração
public class OrderService {
    private readonly INotifier _notifier;
    public OrderService(INotifier n) => _notifier = n;

    public async Task Confirm(Order o) {
        // ... regra de negócio
        await _notifier.Notify(o.Email, "Pedido confirmado");
    }
}

Acoplamento de dados: OrderService depende só do contrato. Pode trocar SMTP, SES, SendGrid sem tocar na regra.

Python
from typing import Protocol

class Notifier(Protocol):
    def notify(self, to: str, msg: str) -> None: ...

class OrderService:
    def __init__(self, notifier: Notifier):
        self._notifier = notifier

    def confirm(self, order: Order) -> None:
        # ... regra de negócio
        self._notifier.notify(order.email, "Pedido confirmado")

Mesma estrutura. Protocol dá structural typing — qualquer classe com notify serve, sem herança nominal.

Go
// Interface no consumidor (idioma Go)
type notifier interface {
    Notify(to, msg string) error
}

type OrderService struct {
    notifier notifier
}

func (s *OrderService) Confirm(o Order) error {
    // ... regra de negócio
    return s.notifier.Notify(o.Email, "Pedido confirmado")
}

Interface minúscula, definida onde é usada. Quem implementa Notify(string, string) error serve — sem registro nominal.

Em todas as três, o ganho é o mesmo: OrderService fica com acoplamento de dados, dependendo apenas do contrato mínimo. Mas Go vai um passo além no idioma: definir a interface no consumidor, não no produtor, reduz acoplamento aferente da implementação concreta — ela não precisa "saber" que está sendo usada como notifier. Esse é o tipo de detalhe que diferencia design idiomático de design transposto.

Quando aceitar acoplamento alto — o lado pragmático

Como tudo em design de software, a busca por baixo acoplamento tem custo. Toda vez que você introduz uma interface para inverter dependência, está adicionando indireção: um arquivo a mais, uma classe a mais, mais código para um leitor navegar até chegar à implementação real. Em sistemas pequenos ou em código com ciclo de vida curto, esse custo pode ser maior que o benefício.

A heurística é: aceite acoplamento alto onde a chance de mudar é mínima. Um wrapper fino sobre System.IO.File em C# ou os.path em Python existe principalmente para testabilidade, e às vezes nem isso vale: as APIs de stdlib são estáveis há décadas, e o wrapper só adiciona ruído. Já uma interface sobre o cliente de Postgres tem ganho real, porque é provável que você queira testar sem subir banco, ou trocar para um pool diferente, ou adicionar instrumentação.

O sinal de que vale inverter dependência: você consegue nomear pelo menos uma mudança plausível que a inversão habilita. "Posso testar sem precisar do recurso real", "posso trocar a implementação concreta", "posso adicionar instrumentação sem tocar a regra de negócio". Se não consegue nomear nada concreto, está provavelmente abstraindo por hábito, não por razão.

Como praticar

A maneira mais eficaz de internalizar coesão e acoplamento é fazer auditoria em código real. Dois exercícios funcionam bem:

  1. Diagrama de dependências. Pegue um projeto seu de 20-30 arquivos. Para cada arquivo, anote quais outros arquivos ele importa (Ce) e quantos importam ele (Ca). Identifique candidatos a "deus de classe" (Ca alto + Ce alto). Identifique abstrações estáveis (Ca alto + Ce baixo). A própria atividade de mapear muda como você lê o sistema.
  2. Análise post-mortem de bugs. Pegue os últimos 5 bugs sérios que você corrigiu. Para cada um, pergunte: a correção exigiu mudar quantos arquivos? Por quê? Em quase todos os casos onde o número for alto, há acoplamento mal-resolvido por trás. Identificar esses pontos é mais barato do que reescrever tudo, e dá clareza sobre onde investir refactoring.

No projeto deste módulo (URL Shortener), os pontos onde coesão e acoplamento aparecem são: a separação entre Domain, Application e Infrastructure (acoplamento inverso via interfaces), a coesão de cada serviço (cada um faz uma coisa relacionada), e a forma como você passa dados entre camadas (acoplamento de dados, não de estrutura inteira). Faça essas três decisões com consciência — não por costume — e o exercício já vale.

Referências para aprofundar

  1. livro Structured Design — Edward Yourdon & Larry Constantine (1979). A fonte original. Capítulos 6 e 7 trazem as taxonomias completas de coesão e acoplamento. Leitura datada na forma, atual no conteúdo.
  2. livro Clean Architecture: A Craftsman's Guide to Software Structure and Design — Robert C. Martin (2017). Capítulos 13-14 introduzem Ca/Ce e a noção de "Stable Abstractions Principle".
  3. livro A Philosophy of Software Design — John Ousterhout (2018, 2nd ed. 2021). Defende "deep modules" — interfaces pequenas escondendo implementações grandes. Versão moderna e operacional dos mesmos princípios.
  4. livro Domain-Driven Design Distilled — Vaughn Vernon (2016). Aplicação prática dos conceitos em escala arquitetural: bounded contexts são coesão; contexts maps gerenciam acoplamento.
  5. artigo Clean Coder Blog: The Principles of OOD — Robert C. Martin. butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod — discute Ca/Ce e o conceito de Distance from Main Sequence. Curto e direto.
  6. artigo Connascence — A Better Way to Measure Coupling — Jim Weirich. Refinamento moderno da taxonomia de Constantine. Connascence dá oito categorias mais granulares de acoplamento. Vale conhecer.
  7. artigo Mocks Aren't Stubs — Martin Fowler. martinfowler.com/articles/mocksArentStubs.html — discute como diferentes formas de teste expõem (ou escondem) acoplamento. Companhia útil.
  8. docs Microsoft .NET Application Architecture Guides. learn.microsoft.com/en-us/dotnet/architecture — referência prática de como aplicar inversão de dependência em .NET 10.
  9. docs Effective Go — Package Names & Interfaces. go.dev/doc/effective_go#interfaces — explica por que Go define interfaces no consumidor, com exemplos da stdlib.
  10. vídeo The Forgotten Art of Structured Programming — Kevlin Henney. YouTube. Henney resgata Constantine e mostra como muito do "novo" em design moderno é redescoberta. Imperdível.
  11. vídeo The Mess We're In — Joe Armstrong (2014). YouTube. O criador do Erlang fala sobre acoplamento e o estado da indústria. Provocativo e formativo.
  12. paper On the Criteria To Be Used in Decomposing Systems into Modules — David Parnas (1972). CACM 15(12). O paper que fundou a discussão de modularidade. Curto, profundo, ainda atual depois de meio século.