Trilha 2 · Modularização e complexidade · ~22 min
"Acoplamento é ruim" é uma das frases mais repetidas e menos úteis da nossa área. Ruim como? Todo sistema tem acoplamento — se dois módulos nunca interagem, eles são dois sistemas. O problema nunca foi ter acoplamento; foi ter o tipo errado, na força errada, no lugar errado.
Aprendi isso quebrando produção. Numa migração, "desacoplamos" dois serviços colocando uma fila entre eles. Ficou lindo no diagrama. Só que trocamos um acoplamento forte e óbvio (chamada de função) por um acoplamento temporal difuso: agora os dois serviços dependiam de ordem de mensagens, de idempotência, de a fila estar no ar — e quando algo dava errado, ninguém sabia onde. "Desacoplamos" no papel e acoplamos de um jeito muito pior na prática. Faltava vocabulário para enxergar que existem dimensões de acoplamento, e que reduzir uma pode aumentar outra.
Acoplamento não é um número, são três eixos
A grande sacada moderna sobre acoplamento (sistematizada por Vlad Khononov em Balancing Coupling in Software Design) é que ele não é uma quantidade única. São três eixos que você avalia juntos:
- Força (strength): quanto um módulo precisa saber sobre o outro para funcionar. Vai de "sabe tudo por dentro" a "sabe só um dado".
- Distância (distance): quão longe os módulos acoplados estão na estrutura — mesma classe, mesmo módulo, módulos diferentes, serviços diferentes, sistemas de organizações diferentes.
- Volatilidade/tempo (volatility & temporal): quanto muda e quando/em que ordem as coisas precisam acontecer.
A regra que amarra os três: acoplamento é aceitável quando força × distância × volatilidade é baixo. Um acoplamento forte é ok se a distância é curta e a volatilidade é baixa (dois métodos da mesma classe podem se conhecer intimamente). O mesmo acoplamento forte entre dois serviços de times diferentes, sobre algo que muda toda semana, é uma bomba.
Insight central: o pecado não é acoplar. É acoplar com força alta a uma distância grande sobre algo volátil. Todo o resto da aula é aprender a enxergar essas três medidas.
Os tipos de acoplamento por FORÇA
Do pior (mais forte) ao melhor (mais fraco). No FakeFlix, imagine Content precisando saber do status da assinatura em Billing.
Content coupling (o pior) — um módulo mexe nas tripas do outro
// ❌ Content altera diretamente o estado interno de Billing
billingModule.subscriptions[userId].status = 'active';
Content depende da representação interna de Billing (que existe um array subscriptions, indexado por userId, com um campo status). Qualquer mudança interna de Billing quebra Content. Força máxima.
Common coupling — dependem de um estado global compartilhado
// ❌ ambos leem/escrevem o mesmo global mutável
globalState.currentUserPlan = 'premium'; // Billing escreve
if (globalState.currentUserPlan === 'premium') { /* Content lê */ }
O appState da aula Complexidade essencial vs. acidental e carga cognitiva. Mudanças no formato do global afetam todos. Força alta e efeito difuso.
Control coupling — um módulo controla o fluxo do outro por uma flag
// ❌ Content diz a Billing COMO se comportar, não O QUE quer
billing.process(userId, { mode: 'checkOnly', skipCache: true, recalcTax: false });
Content precisa conhecer os modos internos de Billing e passar flags que ligam/desligam comportamento. Content sabe demais sobre o como de Billing.
Stamp coupling — passa uma estrutura inteira quando só precisa de um pedaço
// ❌ Content recebe o objeto Subscription inteiro só para olhar 1 campo
function canWatch(subscription: Subscription): boolean {
return subscription.status === 'active'; // usa 1 de 20 campos
}
Content agora depende do formato de Subscription inteiro; se Billing renomear qualquer campo dessa estrutura, o contrato muda. Força média.
Data coupling (o alvo) — troca só o dado necessário
// ✅ Content pergunta uma coisa, recebe uma coisa
const active: boolean = await billingApi.isUserSubscriptionActive(userId);
Content passa só o userId e recebe só um boolean. Não sabe o que é uma Subscription, nem seus campos, nem seu status interno. É o acoplamento mais fraco possível sem deixar de interagir. Billing pode mudar quase tudo por dentro sem tocar Content. Esse é o destino de todo acoplamento entre módulos.
Observe que o Public API Provider da aula Comunicação entre módulos: o padrão Public API Provider existe justamente para transformar as formas fortes acima em data coupling. Ele é a ferramenta que empurra o acoplamento para o fim bom da escala.
A dimensão DISTÂNCIA: a regra de ouro
A mesma força de acoplamento tem gravidade diferente conforme a distância:
| Distância | Acoplamento forte aqui é… |
|---|---|
| Dois métodos da mesma classe | Ok — coesão local, muda junto de qualquer jeito |
| Duas classes do mesmo módulo | Tolerável — mesma fronteira, mesmo dono |
| Dois módulos diferentes | Perigoso — precisa ser fraco (data coupling) |
| Dois serviços/times diferentes | Grave — precisa ser fraquíssimo e versionado |
| Dois sistemas de orgs diferentes | Crítico — contrato público, mudança negociada |
A regra: quanto maior a distância, mais fraco o acoplamento deve ser. Dentro de uma classe você pode se dar ao luxo de acoplamento forte (é barato, muda junto). Entre serviços de times diferentes, só data coupling versionado sobrevive.
Isso explica por que a árvore do FakeFlix separa core/http/persistence dentro de um módulo (distância curta, acoplamento mais forte tolerado) mas exige Public API entre módulos (distância grande, só data coupling). A estrutura de pastas codifica a regra da distância.
A dimensão TEMPO: o acoplamento invisível
O mais traiçoeiro, porque não aparece nos imports. Acoplamento temporal é quando a corretude depende de ordem ou de coincidência no tempo.
// ❌ acoplamento temporal escondido
service.loadConfig(); // se esquecer isto...
service.connect(); // ...isto explode. A ordem é uma regra não escrita.
service.start();
Nada nos tipos diz que loadConfig precisa vir antes. É uma dependência de ordem — pura carga cognitiva (aula Complexidade essencial vs. acidental e carga cognitiva) e fonte de bugs de "funciona na minha máquina". Outra forma:
// ❌ dependem de estarem no ar AO MESMO TEMPO
const plan = await billingHttpClient.get(`/plan/${userId}`); // se Billing estiver fora, Content cai
Aqui Content e Billing têm acoplamento temporal síncrono: precisam estar disponíveis simultaneamente. Foi exatamente o que minha "fila mágica" da abertura tentou resolver — e trocou por um acoplamento temporal pior (ordem e entrega de mensagens).
O ponto: assíncrono/eventos reduzem acoplamento temporal síncrono (não precisam estar no ar juntos) mas introduzem outro tipo temporal (ordem, idempotência, consistência eventual). Não existe eliminar acoplamento temporal de graça — você troca de forma. Escolha a forma cujo custo você sabe pagar (aulas Subdomínios e comunicação assíncrona (BullMQ): mensagens vs. eventos e Event-Driven na prática: Transactional Outbox, idempotência e sagas).
Antes → Depois (empurrando o acoplamento para o eixo bom)
| Antes | Dimensão ruim | Depois | Resultado |
|---|---|---|---|
billing.subscriptions[id].status = 'active' |
Força máxima (content) | billingApi.isUserSubscriptionActive(id) |
Data coupling |
Passar Subscription inteiro |
Stamp | Passar userId, receber boolean |
Data coupling |
Import de content/ para billing/core/ |
Força + distância | Falar via Public API | Distância protegida |
load(); connect(); start() por ordem |
Temporal | Um método que orquestra internamente | Ordem encapsulada |
Tradeoffs
- Reduzir uma dimensão pode aumentar outra. Foi a lição da abertura: eliminar acoplamento síncrono (temporal) via fila aumentou complexidade e criou acoplamento de ordem. Sempre pergunte: "reduzi qual eixo, e qual eixo piorou?".
- Data coupling tem custo. Empurrar tudo para "troca o dado mínimo" às vezes gera muitas chamadas granulares (chattiness) e perda de contexto. Em fronteiras muito conversadas, um contrato um pouco mais gordo (mais próximo de stamp) pode ser pragmático — desde que versionado.
- Nem todo acoplamento forte é ruim. Distância curta + baixa volatilidade tornam acoplamento forte perfeitamente saudável. Não gaste energia "desacoplando" dois métodos da mesma classe — é otimização no lugar errado.
Cola mental
- Acoplamento tem 3 eixos: força (quanto sabe), distância (quão longe), tempo/volatilidade (ordem e quanto muda).
- Aceitável quando força × distância × volatilidade é baixo.
- Escala de força (pior→melhor): content → common → control → stamp → data coupling (alvo entre módulos).
- Regra de ouro: maior distância ⇒ acoplamento mais fraco.
- Acoplamento temporal é o invisível (ordem, "estar no ar junto"). Async não o elimina — troca de forma.
- Public API Provider (aula Comunicação entre módulos: o padrão Public API Provider) é a ferramenta que empurra acoplamento entre módulos para data coupling.
Referências para Aprofundamento
A taxonomia
- Structured Design — Larry Constantine e Ed Yourdon — O artigo de 1974 que classificou acoplamento em content, common, control, stamp e data. Meio século depois, é a mesma escala usada nesta aula.
- Balancing Coupling in Software Design — Vlad Khononov — O tratamento moderno e o mais próximo desta aula: acoplamento como grandeza de múltiplas dimensões, com força, distância e volatilidade.
Medir em vez de opinar
- Your Code as a Crime Scene — Adam Tornhill — Ensina a extrair coacoplamento do histórico do repositório: quais arquivos mudam juntos, mesmo sem dependência declarada. É a evidência objetiva que a aula pede.
- Code Maat — A ferramenta do livro, de linha de comando, para rodar essa análise no seu próprio repositório hoje.
- Coupling Is the Biggest Challenge in Software Engineering — Nick Tune — Ensaio curto sobre por que a decisão de fronteira é a decisão de acoplamento, e por que ela é organizacional tanto quanto técnica.
Exercícios
Exercício 1 — Nomeie o tipo
Classifique a força do acoplamento (content/common/control/stamp/data):
(a) content.render(user) recebe o objeto User completo mas só usa user.name;
(b) billing.run(userId, { dryRun: true });
(c) identity.sessions[token] = {...} acessado por outro módulo;
(d) const ok = await identityApi.isTokenValid(token).
Ver resposta
✓ Resposta: (a) Stamp — recebe a estrutura User inteira, usa um campo; depende do formato. (b) Control — a flag dryRun controla o comportamento interno de billing; o chamador conhece o "como". (c) Content coupling — acesso direto à estrutura interna sessions. (d) Data coupling — passa um token, recebe um boolean; ideal.
Exercício 2 — Ordene a gravidade
O mesmo acoplamento forte (stamp) aparece em três lugares: (i) entre dois métodos privados da mesma classe; (ii) entre dois módulos do monolito; (iii) entre dois serviços de times diferentes deployados separadamente. Ordene do menos grave ao mais grave e explique com a regra da distância.
Ver resposta
✓ Resposta: Do menos ao mais grave: (i) mesma classe → (ii) mesmo monolito → (iii) serviços de times diferentes. Pela regra da distância, a mesma força de acoplamento pesa mais quanto maior a distância. Em (i) os dois métodos mudam juntos, mesmo dono, custo trivial de manter em sincronia. Em (ii) já há fronteira de módulo, então stamp começa a incomodar (Content passa a depender do formato de uma estrutura de Billing), mas ainda é um deploy só. Em (iii) o stamp vira contrato distribuído entre times: mudar o formato exige coordenação, versionamento e deploy sincronizado — a forma mais cara de todas. Mesma força, gravidades muito diferentes.
Exercício 3 — Ache o acoplamento temporal
O que há de frágil aqui, e por quê não aparece nos tipos?
cache.warmUp();
const result = report.generate(); // depende de o cache já estar quente
mailer.send(result);
Ver resposta
✓ Resposta: report.generate() depende, silenciosamente, de cache.warmUp() já ter terminado — é acoplamento temporal de ordem. Não aparece nos tipos porque a assinatura de generate() não declara essa pré-condição; ela é uma regra não escrita que vive só na sequência das linhas. Se alguém reordenar, remover ou paralelizar, o relatório sai vazio ou errado sem erro de compilação. Correção: encapsular a dependência (ex.: generate() garante internamente que o cache está quente, ou recebe os dados de que precisa como argumento) de modo que a ordem não seja responsabilidade do chamador.
Exercício 4 — A fila mágica
Um colega quer eliminar o acoplamento temporal síncrono entre Content e Billing metendo uma fila. Qual dimensão de acoplamento ele reduz, qual ele introduz, e que três problemas novos ele passa a ter que resolver?
Ver resposta
✓ Resposta: Ele reduz o acoplamento temporal síncrono (Content e Billing não precisam mais estar no ar ao mesmo tempo; Content publica/consome e segue). Ele introduz acoplamento temporal assíncrono/de ordem e move para consistência eventual. Três problemas novos: (1) Idempotência — a mesma mensagem pode chegar duas vezes; o consumidor precisa não processar em dobro (não cobrar duas vezes). (2) Ordenação/entrega — mensagens podem chegar fora de ordem ou se perder; é preciso garantir entrega (ex.: Transactional Outbox, aula Event-Driven na prática: Transactional Outbox, idempotência e sagas). (3) Observabilidade/consistência eventual — há uma janela em que os dados divergem, e debugar um fluxo assíncrono é muito mais difícil que seguir uma chamada de função; precisa de tracing e tratamento de falha. Moral: ele não "eliminou" acoplamento — trocou por uma forma cujo custo operacional só vale se a escala/autonomia justificar.
Exercício 5 — Desafio: mova no eixo certo
Content precisa saber o plano do usuário para escolher a resolução do vídeo. Hoje o código é este:
// content/core/service/streaming.service.ts
import { SubscriptionEntity } from '../../../billing/persistence/entity/subscription.entity';
async resolveQuality(userId: string): Promise<'sd' | 'hd' | '4k'> {
const sub: SubscriptionEntity = await this.billingRepo.findOne({ where: { userId } });
if (sub.plan.tier === 'premium' && sub.status === 'active') return '4k';
if (sub.status === 'active') return 'hd';
return 'sd';
}
Classifique o acoplamento nos três eixos. Depois proponha a mudança que dá o maior ganho pelo menor custo — e diga qual eixo ela move e qual ela não move.
Ver resposta
✓ Resposta: Força: stamp, beirando content. Content importa a SubscriptionEntity inteira de billing/persistence/ e ainda navega a estrutura interna dela (sub.plan.tier, sub.status), interpretando por conta própria o que "ativo" e "premium" significam. Qualquer renomeação de coluna, mudança de enum ou introdução de trial quebra Content em silêncio. Distância: grande — são dois bounded contexts diferentes, e o import atravessa direto para a camada de persistência do vizinho, pulando qualquer contrato. Força alta a distância grande é justamente a combinação venenosa da aula. Tempo: acoplamento síncrono em runtime — a chamada acontece no caminho do play; se Billing estiver fora do ar ou lento, o vídeo não começa. Maior ganho pelo menor custo: expor uma Public API em Billing que devolva o dado mínimo já interpretado — por exemplo getEntitlement(userId): Promise<'none' | 'standard' | 'premium'> — e fazer Content depender só disso. Isso derruba a força de stamp para data coupling (trafega um valor, não uma estrutura) e tira o import de persistence/, que é onde estava o pior. O significado de "ativo" e "premium" volta a viver dentro de Billing, que é quem tem autoridade sobre ele. O que essa mudança não move: o eixo tempo. Continua sendo uma chamada síncrona no caminho do play, e Billing indisponível continua derrubando a decisão de qualidade. Resolver o eixo tempo exige eventos e cache local de entitlement — mais caro, e só se paga com evidência de escala ou de indisponibilidade real. O ponto do exercício é esse: os três eixos se movem separadamente, e tratar "desacoplar" como uma coisa só leva a pagar por um eixo que ainda não estava doendo.