Pular para o conteúdo
10 min de leitura

Acoplamento e coesão: os dois eixos que decidem a arquitetura do seu software

Por Equipe Tech do Sonne ·

Antes de escolher microsserviços ou monólito, entenda acoplamento e coesão — os dois princípios que realmente determinam se seu sistema será fácil ou impossível de evoluir.

Neste artigo

Arquitetura é sobre o que você consegue mudar sem medo#

Toda discussão de arquitetura de software acaba, mais cedo ou mais tarde, virando uma disputa de rótulos: monólito contra microsserviços, camadas contra hexagonal, orientado a eventos contra requisição-resposta. Essas escolhas importam, mas são derivadas. Por baixo de qualquer estilo arquitetural há dois princípios mais antigos e mais fundamentais, formulados ainda nos anos 1970 e nunca desbancados: baixo acoplamento e alta coesão. Entendê-los é a diferença entre desenhar sistemas de propósito e apenas copiar diagramas.

A pergunta que a arquitetura precisa responder não é "quantos serviços?" nem "qual framework?". É: quanto do sistema eu preciso entender e alterar para fazer uma mudança? Um bom desenho mantém essa resposta pequena e previsível. Um desenho ruim faz com que trocar a cor de um botão exija recompilar o motor de faturamento. A qualidade de uma arquitetura se mede pelo raio de impacto de uma mudança típica, e é exatamente isso que acoplamento e coesão governam.

Coesão: manter junto o que muda junto#

Coesão descreve o quão relacionadas estão as responsabilidades dentro de um mesmo módulo. Um módulo coeso faz uma coisa bem definida; tudo dentro dele existe para servir a um propósito comum. Um módulo de baixa coesão é um depósito — funções que foram parar juntas por acaso histórico, não por afinidade.

O sintoma clássico de baixa coesão é a classe utilitária que cresce sem limite: Utils, Helpers, Manager, Service genérico onde tudo que ninguém sabia onde colocar acabou. Esses arquivos são ímãs de complexidade porque não têm fronteira conceitual: não existe critério para dizer o que pertence e o que não pertence a eles.

O guia prático mais poderoso para coesão vem do Princípio da Responsabilidade Única, e sua formulação mais precisa não é "uma classe faz uma coisa" — é "um módulo deve ter uma, e apenas uma, razão para mudar". A razão para mudar é sempre uma pessoa ou um papel: o time de faturamento pede uma mudança, o time de compliance pede outra. Se as regras de faturamento e as regras fiscais vivem na mesma classe, dois grupos distintos vão editar o mesmo arquivo por motivos distintos — e cada um corre o risco de quebrar o do outro.

Alta coesão, portanto, significa organizar o código por motivo de mudança, não por semelhança técnica. Um erro comum é agrupar "todos os controllers", "todos os repositórios", "todos os models" em pastas separadas. Isso otimiza para a pergunta errada: "onde ficam os controllers?" quando a pergunta real do dia a dia é "onde está a lógica de pedidos?". Estruturar por feature — tudo que diz respeito a pedidos junto, tudo que diz respeito a pagamentos junto — coloca sob o mesmo teto as coisas que mudam pelo mesmo motivo.

Acoplamento: as amarras entre os módulos#

Se coesão é sobre o que fica junto dentro de um módulo, acoplamento é sobre quão fortemente os módulos dependem uns dos outros. Dois módulos estão acoplados quando uma mudança em um força uma mudança no outro. Algum acoplamento é inevitável e desejável — módulos precisam colaborar. O problema é o acoplamento excessivo, oculto ou desnecessário, que faz o sistema se comportar como um bloco monolítico mesmo quando o diagrama sugere separação.

Nem todo acoplamento é igual. Alguns tipos são muito mais custosos que outros:

  • Acoplamento de dados (o mais leve): um módulo passa dados simples a outro por uma interface bem definida. É o acoplamento que você quer.
  • Acoplamento de controle: um módulo passa uma flag que altera o comportamento do outro, obrigando-o a conhecer detalhes internos do chamado.
  • Acoplamento comum: vários módulos compartilham estado global mutável. Uma mudança nesse estado se propaga de formas imprevisíveis, e ninguém consegue raciocinar sobre um módulo isolado.
  • Acoplamento de conteúdo (o mais grave): um módulo depende dos detalhes internos de outro — mexe em seus campos privados, assume sua estrutura de dados interna. Qualquer refatoração do módulo interno quebra o externo.

O acoplamento mais perigoso costuma ser o invisível: o acoplamento temporal (o módulo A precisa ser chamado antes do B, mas nada no código expressa isso) e o acoplamento por conhecimento compartilhado de um esquema — dois serviços que leem a mesma tabela do banco estão profundamente acoplados, mesmo sem nunca se chamarem diretamente. Uma mudança no esquema quebra os dois, e o compilador não avisa ninguém.

Inversão de dependência: apontar as setas para dentro#

A ferramenta mais eficaz para controlar acoplamento é a inversão de dependência. A ideia é simples de enunciar e transformadora na prática: módulos de alto nível (a lógica de negócio) não devem depender de módulos de baixo nível (banco de dados, APIs externas, frameworks). Ambos devem depender de abstrações.

Considere um serviço de notificação. A versão ingênua acopla a regra de negócio diretamente ao provedor de e-mail:

```typescript // Acoplado: a lógica de negócio conhece o provedor concreto class ServicoDeContas { private email = new ProvedorSendgrid(); // dependência rígida

async criarConta(dados: DadosConta) { const conta = await this.salvar(dados); await this.email.enviar(conta.email, "Bem-vindo!"); return conta; } } ```

Trocar o Sendgrid por outro provedor, ou testar criarConta sem disparar e-mails de verdade, exige mexer na regra de negócio. Agora com a dependência invertida:

```typescript // Desacoplado: a lógica depende de uma abstração que ela define interface Notificador { enviar(destino: string, mensagem: string): Promise<void>; }

class ServicoDeContas { constructor(private notificador: Notificador) {} // injetado

async criarConta(dados: DadosConta) { const conta = await this.salvar(dados); await this.notificador.enviar(conta.email, "Bem-vindo!"); return conta; } } ```

Repare no detalhe crucial: a interface Notificador pertence à camada de negócio, não à camada de infraestrutura. É a lógica de negócio que dita o contrato — "eu preciso de algo capaz de notificar" — e a implementação concreta (Sendgrid, SMTP, um fake de teste) se conforma a esse contrato. A seta de dependência aponta para dentro, em direção ao domínio. Essa é a ideia que sustenta arquiteturas como a hexagonal (portas e adaptadores) e a clean architecture: o núcleo de regras de negócio não conhece o mundo externo; o mundo externo se pluga nele.

O benefício vai além de trocar provedores. Ele torna a regra de negócio testável em isolamento, sem banco, sem rede, sem framework. Módulos que dependem de abstrações são módulos que você consegue exercitar sozinhos — e a testabilidade é um dos indicadores mais confiáveis de bom acoplamento. Se testar uma classe exige montar meio sistema, o acoplamento está alto, e nenhum diagrama bonito muda esse fato.

Fronteiras: onde separar e onde não#

Com acoplamento e coesão em mente, a decisão sobre fronteiras — módulos, pacotes, serviços — deixa de ser dogma e vira análise. A regra é traçar fronteiras onde o acoplamento é naturalmente baixo e a coesão interna é alta. Você quer que a fronteira corte por uma junta fina do sistema, não pelo meio de um osso.

O conceito de bounded context, vindo do design orientado a domínio, formaliza isso: um contexto delimitado é uma região onde um modelo e um vocabulário são consistentes. "Cliente" no contexto de vendas e "cliente" no contexto de suporte são conceitos diferentes com atributos diferentes; tentar unificá-los numa única entidade compartilhada cria acoplamento entre times que deveriam evoluir independentemente. Fronteiras bem colocadas acompanham essas descontinuidades de significado.

É aqui que a decisão monólito-versus-microsserviços encontra seu lugar correto. Microsserviços são uma forma de tornar fronteiras de módulo em fronteiras de rede e de deploy independente. Mas eles só funcionam se as fronteiras já forem boas: se você extrair um serviço em cima de um acoplamento alto, transforma uma chamada de função em uma chamada de rede — mais lenta, mais frágil, mais difícil de depurar — sem eliminar a dependência. O resultado é o pior dos mundos, um "monólito distribuído", onde você paga o custo operacional da distribuição sem ganhar a independência que a justificaria.

A sequência saudável é quase sempre a inversa da hype: comece com um monólito bem modularizado, com fronteiras internas claras e baixo acoplamento entre módulos. Deixe o sistema e o entendimento do domínio amadurecerem. Extraia serviços apenas quando houver uma razão concreta — escala independente, cadência de deploy diferente, isolamento de falha — e apenas nas juntas que já provaram ser fronteiras reais. Distribuir cedo é comprar complexidade a crédito.

Sinais concretos de acoplamento ruim#

A teoria de acoplamento e coesão vira útil quando você aprende a reconhecer seus sintomas no código real, antes que virem crises. Alguns sinais de alerta são confiáveis o suficiente para funcionar como um radar durante a revisão de qualquer mudança:

  • O efeito dominó nas mudanças. Você altera um módulo e, inesperadamente, três outros quebram. Se uma mudança localizada tem consequências espalhadas, os módulos estão mais acoplados do que o diagrama sugere. O raio de impacto revela o acoplamento real, não o pretendido.
  • A dificuldade de testar em isolamento. Se para testar uma classe você precisa instanciar meia dúzia de outras, configurar um banco e simular uma API externa, essa classe está acoplada a tudo isso. Testabilidade é um termômetro direto de acoplamento — código fácil de testar isoladamente é código bem desacoplado.
  • A importação que atravessa camadas. Quando um módulo de domínio importa diretamente um driver de banco, um cliente HTTP ou um detalhe de framework, a seta de dependência está apontando para o lado errado. O domínio deveria depender de abstrações que ele define, não de infraestrutura concreta.
  • O parâmetro booleano que muda comportamento. Uma função que recebe uma flag e faz coisas fundamentalmente diferentes conforme seu valor é, na verdade, duas funções disfarçadas — e o chamador precisa conhecer a lógica interna para saber qual flag passar. Isso é acoplamento de controle, e quase sempre pede separação em funções distintas.
  • O objeto anêmico rodeado de manipuladores. Quando os dados vivem numa estrutura passiva e a lógica que os manipula está espalhada por vários serviços externos, a coesão está baixa: o que deveria estar junto — dado e comportamento — foi separado, e cada mudança na estrutura obriga a caçar todos os manipuladores.

Reconhecer esses sinais não exige ferramentas sofisticadas; exige o hábito de perguntar, a cada mudança, "quanto do sistema eu tive que tocar para fazer isto?". Quando a resposta cresce sem que a mudança seja conceitualmente maior, o acoplamento está cobrando seu preço.

O critério que sobrevive às modas#

Frameworks vão e vêm, estilos arquiteturais entram e saem de moda, mas os dois eixos permanecem. Diante de qualquer decisão de design, duas perguntas cabem sempre: isto aumenta a coesão, mantendo junto o que muda junto? e isto reduz o acoplamento, permitindo mudar uma parte sem tocar nas outras? Se a resposta às duas for sim, quase certamente é uma boa decisão, independentemente do rótulo que ela receba.

Vale lembrar que arquitetura não é um documento que se escreve uma vez e se emoldura. Ela é a soma de milhares de decisões pequenas — como estruturar uma pasta, onde colocar uma interface, se um módulo deve conhecer outro. Muitas dessas decisões são discutidas e refinadas justamente na revisão de código do dia a dia, e boa parte da clareza que sustenta um sistema saudável nasce de escrever cada peça com a legibilidade em mente, tema que exploramos em Clean Code na prática. Arquitetura e clean code não são disciplinas separadas: são a mesma preocupação — reduzir o custo de entender e mudar — aplicada em escalas diferentes.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly