Coesão é o que faz os membros de uma classe estarem juntos; acoplamento é o quanto ela precisa saber das outras para funcionar.
A prática vem antes do fundamento
Boa parte do que a gente faz todo dia é a aplicação de um princípio que não sabemos nomear. Quem estrutura sistema com classe de responsabilidade única e dependência externa reduzida ao mínimo já aplica coesão e acoplamento, mesmo sem nunca ter usado as palavras.
Fazer sem saber o termo não é problema: o código sai certo do mesmo jeito. Mas ficar só na prática cobra depois.
Sem o conceito, o que se tem é hábito: você repete o que funcionou porque funcionou. Com o fundamento, você sabe por que funcionou, e é isso que permite decidir num caso que não se parece com nenhum dos anteriores, reconhecer quando aquilo não se aplica, e discutir a decisão com alguém que discorda dela.
Ou seja: não tem técnica nova aqui, e ainda assim vale entender. O que muda não é o que você escreve, é o que você sabe sobre o que escreve.
Uma das consequências aparece no review, e é a mais visível: "essa classe está com coesão baixa" e "isso é acoplamento de controle" dizem o que está errado e onde; "não gostei" não diz. É a diferença entre um apontamento que o autor consegue agir em cima e um que ele só consegue discordar. Mas o review é só onde o entendimento fica aparente, e o ganho é do programador antes de ser da equipe.
Coesão: o que está junto pertence junto?
Coesão mede o que faz os membros de uma classe estarem no mesmo arquivo. Não é sobre tamanho, é sobre motivo.
Do pior para o melhor:
| Nível | O que junta os membros | Como aparece |
|---|---|---|
| Coincidental | Nada. Estão juntos por acaso | Utils, Helpers, Commons |
| Lógica | São "do mesmo tipo" de coisa, e uma flag escolhe qual roda | processar(tipo) com um switch grande |
| Temporal | Rodam no mesmo momento | inicializar() que faz oito coisas sem relação entre si |
| Funcional | Tudo contribui para uma única tarefa | O alvo |
A coesão lógica é a que mais engana. A classe tem nome de responsabilidade única, o cabeçalho promete uma coisa só, e por dentro é um switch decidindo entre três comportamentos que não se falam. Pelo nome, coesa. Pelo conteúdo, três classes.
Acoplamento: quanto uma classe depende das outras
Acoplamento mede o quanto uma classe precisa saber sobre outra para funcionar.
Do pior para o melhor:
| Nível | O que é | Como aparece |
|---|---|---|
| Conteúdo | Uma classe mexe nas tripas da outra | Acessa campo privado, depende de como a outra funciona por dentro |
| Comum | Duas classes compartilham estado global mutável | Singleton com estado, cache compartilhado |
| Controle | Uma passa uma flag que decide o caminho da outra | salvar(pedido, true), em que o true liga um if lá dentro |
| Stamp | Passa o objeto inteiro quando só precisa de um campo | Recebe Pedido para ler só o cep |
| Dados | Passa só o que a outra precisa | calcularFrete(cep, peso) |
Zero acoplamento não é o alvo
Repare no que a escala mede: ela não vai de "acoplado" a "não acoplado". Vai de pior tipo a melhor tipo, e o melhor nível, acoplamento de dados, ainda é acoplamento.
Projeto grande sempre vai ter dependência entre classes, e isso não é defeito: o sistema existe para se comunicar consigo mesmo. Uma classe que não depende de ninguém e da qual ninguém depende não está desacoplada, está sobrando.
Então a pergunta útil nunca foi "como eliminar a dependência". É de que tipo ela vai ser.
O movimento: a dependência vira contrato
O que se faz na prática para diminuir acoplamento é deixar a dependência da classe como um contrato. A classe passa a depender do que precisa que exista, não de quem vai fazer. Quando surge a necessidade de trocar a implementação, cria-se uma nova e injeta.
// Não é do provedor de e-mail que o caso de uso depende, e sim do contrato.
interface EnviadorDeEmail {
enviar(para: string, assunto: string): void;
}
class CriarConta {
private email: EnviadorDeEmail;
constructor(email: EnviadorDeEmail) {
this.email = email;
}
// Trocar o provedor é passar outro objeto aqui. A classe não muda.
}Vale dizer com todas as letras que isso não é uma técnica deste tópico. É o DIP chegando pelo outro lado: lá o contrato aparece como regra de direção da dependência; aqui ele aparece como a resposta à pergunta "como se reduz acoplamento". Mesmo movimento, duas motivações.
A régua é o teste
O problema prático de "mantenha alta coesão" é que ninguém diz quanto é alta. Não existe número.
O que dá para medir é caminho. Cada alternativa booleana dentro de um método é mais um expoente no número de caminhos possíveis daquela classe: dois if independentes já são quatro fluxos, três são oito.
E o instrumento que expõe isso é o teste unitário. Escrever o teste do método obriga a enumerar as possibilidades: não dá para cobrir o que não se enxergou. É por isso que o teste mede a coesão sem que ninguém tenha combinado isso: a classe incoerente aparece como uma bateria de casos que não se parecem.
Daí sai o critério, sem parâmetro exato: quando está muito difícil de testar e de pegar todos os cenários, provavelmente a classe podia ser quebrada em outras mais simples.
Não é "a classe está grande demais". É "eu não consigo enumerar o que ela faz".
E os dois lados se encontram aqui: o acoplamento de controle, a flag que decide o caminho lá dentro, é exatamente o que multiplica os caminhos a testar. O nível ruim de acoplamento produz a classe difícil de testar.
O que a teoria cobra
Acoplamento e coesão não são as únicas formas de dizer se um código está ruim, e a página perde o valor se for lida assim. Como parâmetro de organização em nível de classe e de método, valem. Como critério único de qualidade, não.
E há um custo que raramente é dito: manter um projeto nessa linha exige processo. O time precisa estar muito alinhado sobre como as coisas devem ser feitas, e é preciso supervisão técnica ativa para o código continuar no que foi planejado. Não é um princípio que se adota escrevendo no documento de padrões: sem esse acompanhamento, o código volta ao normal em poucos sprints.
A falta de régua é um fato, não um problema a resolver
Existem responsabilidades grandes que acabam precisando ser quebradas, e aí fica genuinamente complexo definir o quão coeso é coeso o bastante.
Isso não tem solução, e não precisa ter. É teoria: a tese de que código mais desacoplado organiza melhor é real e se sustenta, mas o que define coesão e acoplamento diante de um número infinito de situações vai dar respostas distintas conforme a situação do projeto. Duas equipes competentes podem discordar sobre a mesma classe, e as duas terem razão dentro do próprio contexto.
Aceitar isso é o que impede a régua de virar burocracia.
Trade-offs
| Ganha | Paga |
|---|---|
| Entender o fundamento do que já se faz por hábito, e conseguir apontar o problema em vez de opinar sobre ele | Processo: time alinhado e supervisão técnica constante |
| Classe mais fácil de testar, porque tem menos caminhos | Mais classes, mais arquivos, mais indireção |
| Trocar implementação sem tocar em quem usa | Contrato a mais para manter em cada troca prevista |
| Critério para decidir onde quebrar uma responsabilidade grande | Nenhum limite objetivo: a decisão continua sendo julgamento |
Princípios relacionados
- Single Responsibility (SRP): o SRP é coesão vista de dentro de uma classe. "Um motivo para mudar" é a mesma pergunta de "o que faz esses métodos estarem juntos", feita pelo lado de quem vai alterar o código.
- Dependency Inversion (DIP): é a técnica que baixa o acoplamento, porque a dependência vira contrato. Aqui está o porquê; lá, a regra.
- Composição vs. Herança: herança é o acoplamento mais forte que existe entre duas classes. A subclasse enxerga o que é
protectede passa a depender de como o pai funciona por dentro: é acoplamento de conteúdo com bênção da linguagem. - Interface Segregation (ISP): interface grande acopla o cliente a métodos que ele não usa. Segregar é baixar acoplamento pelo lado do contrato.
Referências
- Larry Constantine e Edward Yourdon. Structured Design, Prentice Hall, 1979. A origem das duas escalas.
- Thomas McCabe. A Complexity Measure, IEEE Transactions on Software Engineering, 1976. Complexidade ciclomática: o número de caminhos independentes de um método é o número mínimo de testes para cobri-lo.