Design de software

[Modularização #1] O que é modularização? (coesão e acoplamento)

Quanto maior o projeto, mais assustador fica alterar um único arquivo. Afinal, não dá para estimar até onde o impacto vai se espalhar.

5 min de leitura
Imagem de capa de [Modularização #1] O que é modularização? (coesão e acoplamento)

Quanto maior o projeto, mais assustador fica alterar um único arquivo. Afinal, não dá para estimar até onde o impacto vai se espalhar.

O build fica mais lento e o escopo do code review aumenta. Quem acabou de entrar no time também não sabe por onde começar a leitura.

A modularização é a solução mais antiga para esse problema. Em uma frase: é dividir grandes blocos de código em unidades que possam ser entendidas e substituídas de forma independente.

Neste artigo, resumimos o que a modularização realmente divide, os critérios de um bom módulo — alta coesão e baixo acoplamento — e quando fazer essa divisão.

Vamos começar pelo resumo.

  1. Modularização é agrupar o código que «muda junto» e estabelecer limites.
  2. Um bom módulo tem alta coesão e baixo acoplamento.
  3. O objetivo principal é reduzir o custo de mudança, não acelerar o build.
  4. Dividir sem limites claros só aumenta a complexidade.

O que exatamente a modularização divide?

Organizar pastas e dividir módulos são coisas diferentes.

Uma pasta apenas organiza arquivos. O código de qualquer pasta pode usar livremente o código de outra.

Um módulo cria um limite obrigatório. De fora, só é possível usar a interface que o módulo declarou como pública (public).

A essência da modularização não é organizar arquivos, mas fazer o compilador impor o que precisa ser conhecido externamente e o que pode permanecer oculto.

Esse limite possibilita duas coisas.

  • Compreensão independente: basta olhar a interface para usar o módulo, sem conhecer seu interior.
  • Substituição independente: se a interface for a mesma, é possível trocar toda a implementação sem afetar o exterior.

Em 1972, David Parnas apresentou em um artigo o conceito de ocultação de informação (information hiding). O critério para dividir módulos não são as funcionalidades, mas as «decisões de design que queremos ocultar».


Critérios de um bom módulo: coesão e acoplamento

Se dividir os módulos os tornou menos práticos, geralmente essas duas métricas estão desalinhadas.

Coesão indica o quanto o código dentro de um módulo está relacionado. Quanto maior, melhor.

Acoplamento indica o quanto os módulos dependem uns dos outros. Quanto menor, melhor.

Existe uma forma simples de avaliar.

Pergunta Sinal
É preciso alterar vários módulos ao mesmo tempo para corrigir uma funcionalidade? Acoplamento alto
Há código sem relação misturado dentro do módulo? Coesão baixa
Ao excluir um módulo, desaparece exatamente aquela funcionalidade? Está bem dividido

O critério principal é a mudança. O código que muda junto deve ficar em um módulo; o que muda por outros motivos, em outro.

É um princípio conhecido, certo? A «razão da mudança» do princípio da responsabilidade única (SRP) de SOLID foi ampliada para o nível dos módulos.

Fora do módulo, apenas a interface pública fica visível.
Fora do módulo, apenas a interface pública fica visível.

O que melhora com a modularização?

O custo de mudança é o primeiro a diminuir.

Com limites definidos, o impacto de uma alteração fica confinado ao módulo. O revisor pode concluir que o exterior está seguro se a interface pública do módulo não mudou.

A divisão do trabalho entre times também fica mais fácil. Separar responsáveis por módulo reduz conflitos por invadir a área de trabalho uns dos outros.

Os testes também ficam mais leves. É possível testar um único módulo isoladamente, sem iniciar o aplicativo inteiro.

A velocidade do build também melhora, pois basta recompilar o módulo alterado. Mas isso é consequência, não objetivo. Se os limites forem mal projetados, tudo precisará ser recompilado sempre.

Em Swift, os limites aparecem por meio dos modificadores de acesso.

// Apenas protocolos são expostos fora do módulo
public protocol PriceFormatter {
    func format(_ amount: Int) -> String
}

// A implementação fica oculta dentro do módulo
final class KoreanPriceFormatter: PriceFormatter {
    func format(_ amount: Int) -> String { "\(amount)ou" }
}

De fora, basta conhecer PriceFormatter. O ideal é que, mesmo trocando a implementação interna, o código externo ao módulo nem precise ser recompilado.

Os limites são desenhados primeiro no quadro branco, antes do código.
Os limites são desenhados primeiro no quadro branco, antes do código.

Quando dividir e quando esperar?

A modularização não é gratuita. Criar limites traz custos de design de interfaces, gerenciamento de versões e configuração do projeto.

Situação Critério
Uma alteração modifica código de vários times ou áreas É hora de dividir
O build lento interrompe o fluxo de desenvolvimento com frequência Divida, mas projete primeiro os limites de mudança
Produto inicial cujos limites de domínio ainda mudam com frequência Espere até os limites se estabilizarem
Aplicativo pequeno feito por uma única pessoa Organização por pastas e modificadores de acesso são suficientes

Modularizar com limites errados é pior do que não modularizar. Se dois módulos sempre mudam juntos, o limite está errado. O certo é uni-los.

É assim que perguntam em entrevistas

P. Explique coesão e acoplamento e descreva a relação entre eles.

Coesão é a relação entre os elementos internos de um módulo; acoplamento é o grau de dependência entre módulos. Um bom design busca alta coesão e baixo acoplamento: ao reunir o código relacionado, naturalmente diminui o que é trocado com o exterior, reduzindo o acoplamento. É uma relação complementar.

P. Qual critério você usaria para dividir módulos?

Eu usaria a razão da mudança. O código que muda junto pelo mesmo motivo deve ficar em um módulo, enquanto o que muda por motivos diferentes deve ser separado. O limite é correto quando é possível responder qual decisão de design o módulo oculta, e não apenas listar suas funcionalidades.


No próximo artigo, trataremos dos problemas inevitáveis depois de dividir módulos: a direção das dependências entre eles e as dependências circulares.

Projetar as relações depois da divisão é a parte realmente difícil. Com a noção de coesão e acoplamento deste artigo, tudo ficará muito mais fácil.

Artigos recomendados