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.
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:
- Alta coesão, alto acoplamento: módulos bem desenhados internamente, mas que sabem demais uns sobre os outros. Mudança local é fácil, mudança sistêmica é caótica. Sintoma: refatorar um módulo dispara cascata de edições nos outros.
- Baixa coesão, baixo acoplamento: o caso enganador. Cada módulo é independente, sim — mas cada um faz tantas coisas desconexas que ninguém consegue raciocinar sobre eles. É a "salada de funções utilitárias" que parece organizada por estar em arquivos separados, mas na verdade é só desorganização distribuída.
- Baixa coesão, alto acoplamento: o pior cenário possível. Módulos fazem coisas demais e ainda assim dependem uns dos outros. Mudar qualquer coisa quebra qualquer coisa. Esse é o estado terminal de codebases legadas que ninguém mais quer tocar.
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:
// 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.
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.
// 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:
- 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.
- 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
- livro Structured Design — Edward Yourdon & Larry Constantine (1979).
- livro Clean Architecture: A Craftsman's Guide to Software Structure and Design — Robert C. Martin (2017).
- livro A Philosophy of Software Design — John Ousterhout (2018, 2nd ed. 2021).
- livro Domain-Driven Design Distilled — Vaughn Vernon (2016).
- artigo Clean Coder Blog: The Principles of OOD — Robert C. Martin.
- artigo Connascence — A Better Way to Measure Coupling — Jim Weirich.
- artigo Mocks Aren't Stubs — Martin Fowler.
- docs Microsoft .NET Application Architecture Guides.
- docs Effective Go — Package Names & Interfaces.
- vídeo The Forgotten Art of Structured Programming — Kevlin Henney.
- vídeo The Mess We're In — Joe Armstrong (2014).
- paper On the Criteria To Be Used in Decomposing Systems into Modules — David Parnas (1972).