Introdução à modularização
Modularização é a prática de dividir um sistema em módulos: grupos de código que resolvem um problema específico e expõem interfaces bem definidas. Em Java, isso costuma aparecer como pacotes, JARs e, a partir do Java 9, o sistema de módulos nativo; em .NET, como assemblies e namespaces.
A motivação é simples: em um monolito grande, qualquer mudança pode ter efeitos imprevisíveis, porque tudo está misturado. Ao modularizar, cada parte do sistema fica mais fácil de entender, testar, reaproveitar e implantar de forma isolada.
Imagine um e-commerce com autenticação, pedidos e pagamentos. Em um monolito desorganizado, uma alteração no login pode quebrar o fluxo de pedidos. Já em um sistema modular, há módulos separados para autenticação, pedidos e pagamentos, com contratos claros entre eles. Isso permite que cada módulo evolua em ritmos diferentes e, em arquiteturas com microsserviços, cada parte pode ser escalada ou atualizada de forma independente, muitas vezes usando mensageria como Kafka.
Coesão: foco em uma responsabilidade
Coesão mede quão bem as partes internas de um módulo “puxam para o mesmo lado”. Um módulo com alta coesão tem elementos (métodos, classes) todos voltados para um mesmo propósito. Já um módulo com baixa coesão mistura funcionalidades que não têm relação direta.
Na prática, classes e módulos de alta coesão são mais fáceis de testar, entender e refatorar, porque fazem “uma coisa bem feita”. Uma forma comum de quantificar coesão em nível de classe é a métrica LCOM (Lack of Cohesion of Methods), que, em essência, verifica quantos métodos não compartilham atributos. LCOM alto indica baixa coesão, sugerindo que a classe talvez deva ser dividida.
Alguns tipos de coesão, do melhor para o pior, ajudam a avaliar o design:
Funcional: tudo contribui para uma única tarefa.
Exemplo: um módulo CalculoFrete com métodos focados apenas em regras de frete (por CEP, por peso, por transportadora).
Sequencial/Comunicacional: operações que trabalham sobre os mesmos dados em uma sequência lógica.
Exemplo: lerPedido() → calcularFrete() → enviarConfirmacao() usando o mesmo objeto Pedido.
Procedural/Temporal: elementos ligados pela ordem/timing de execução (como rotinas de inicialização).
Lógica: métodos agrupados por “tipo de operação”, mas não necessariamente por um objetivo de negócio (por exemplo, vários utilitários de string em uma mesma classe).
Coincidente: elementos sem relação clara, apenas colocados juntos. Esse é o tipo de coesão mais fraco e deve ser evitado.
Exemplo simplificado em Java:
// Baixa coesão: mistura responsabilidades de domínio, cálculo e infraestrutura
public class PedidoController {
public void criarPedido() { /* lógica de criação / persistência */ }
public void calcularFrete() { /* regra de negócio de frete */ }
public void enviarEmail() { /* integração com e-mail */ } // responsabilidades distintas
}
// Alta coesão: cada classe com foco em uma responsabilidade
@Service
public class CalculoFreteService {
public double calcularPorCep(String cep, double peso) {
return peso * 0.1 + (cep.startsWith("03") ? 5.0 : 10.0);
}
}
Em um cenário de microsserviços, CalculoFreteService pode se tornar um serviço separado, consumindo eventos de um tópico Kafka (pedido-events) e cuidando apenas da parte de frete. Isso aumenta a coesão do serviço e torna suas dependências mais explícitas.
Acoplamento: dependência entre módulos
Se coesão fala sobre “o que está dentro”, acoplamento fala sobre “como módulos se relacionam entre si”. Acoplamento é o grau de dependência entre módulos, classes ou serviços: quanto mais um precisa conhecer detalhes internos do outro para funcionar, maior o acoplamento.
Um certo nível de acoplamento é inevitável: módulos precisam conversar. O problema é o acoplamento excessivo, que torna o sistema frágil: pequenas mudanças em um módulo obrigam alterações em vários outros.
Ca e Ce
Robert C. Martin propõe duas métricas de acoplamento entre módulos:
Ca (Afferent Coupling): quantos módulos dependem deste módulo (“quantos me usam”).
Ce (Efferent Coupling): de quantos módulos este módulo depende (“de quantos eu preciso para funcionar”).
Voltando ao fluxo Pedido → Pagamento → Notificação:
- O módulo Pedido pode depender apenas de Pagamento (Ce = 1), enquanto o frontend depende de Pedido (Ca = 1).
- O módulo Pagamento depende de Notificação e de um gateway externo (Ce = 2), enquanto Pedido e talvez um serviço de cobrança recorrente dependem dele (Ca = 2).
- O módulo Notificação depende de provedores externos (Ce baixo) e é usado por vários módulos (Ca alto), como Pedido, Pagamento e Cadastro.
Mesmo sem calcular números exatos, o exercício de mapear “quem chama quem” já ajuda a visualizar onde o acoplamento está concentrado.
Abstractness (A) e Instability (I)
Além de Ca e Ce, Martin define:
- Abstractness (A): proporção de elementos abstratos (interfaces, classes abstratas) em relação ao total de classes do módulo, sendo A = numero de interfaces + classes abstratas / numero total de classes do modulo. Varia de 0 (totalmente concreto, muito “pé no chão”, pouco flexível) a 1 (totalmente abstrato).
- Instability (I): quão “puxado” o módulo é por suas dependências externas. É calculado como I = Ce/(Ca+Ce), variando de 0 (estável, muitos dependem de mim, dependo de poucos) a 1 (instável, várias dependências e muito flexível, mas talvez pouco utilizável sozinho).
Com isso, surge a ideia da Main Sequence: módulos saudáveis tendem a ficar próximos da linha em que A+I≈1.
- Módulos muito concretos e muito estáveis (A baixo, I baixo) tendem a ficar na “Zona de Dor”: são centrais, muita gente depende deles e qualquer mudança é arriscada.
- Módulos muito abstratos e pouco usados (A alto, I baixo) caem na “Zona de Uselessness”: abstrações sofisticadas, mas sem uso real.
Exemplo concreto com o fluxo Pedido → Pagamento → Notificação
Em um design inicial muito acoplado, o código pode ser assim:
@Service
public class PedidoService {
private final EmailServiceImpl email = new EmailServiceImpl(); // depende da implementação concreta
}
Aqui, PedidoService está fortemente acoplado à implementação específica de e-mail.
Aplicando o Princípio da Inversão de Dependência (DIP), o módulo passa a depender de uma abstração:
public interface NotificacaoService {
void enviar(String mensagem);
}
@Service
public class PedidoService {
private final NotificacaoService notificacao;
public PedidoService(NotificacaoService notificacao) {
this.notificacao = notificacao;
}
}
Com isso:
- O Ce do módulo de pedidos deixa de apontar para a implementação concreta e passa a apontar para uma abstração.
- O módulo de notificação pode ter múltiplas implementações (e-mail, SMS, push), aumentando o Abstractness (A) desse módulo sem aumentar tanto sua Instability (I), já que muitos módulos dependem da interface e poucos detalhes concretos são expostos.
Em uma arquitetura de microsserviços, o acoplamento pode ser reduzido ainda mais se o serviço de pedidos publicar um evento PedidoCriado em um broker (como Kafka) e o serviço de notificação consumir esse evento sem conhecer detalhes internos de Pedido. Assim, o acoplamento entre serviços é mais fraco (assíncrono e orientado a eventos), e cada serviço pode ser implantado e escalado de forma independente.
Connascence: dependências mais sutis
Enquanto acoplamento e coesão olham para dependências mais “macro”, connascence foca em dependências sutis entre elementos: situações em que mudar algo em um lugar exige mudar em outro lugar para que o sistema continue correto.
Existem vários tipos de connascence, mas alguns são especialmente práticos no dia a dia:
Connascence of Name (CoN): dois módulos dependem de usar o mesmo nome.
Exemplo: constantes de status (STATUS_OK = 0) usadas em vários serviços; se o valor ou o nome muda, todos os consumidores precisam ser atualizados.
Connascence of Type (CoT): dependência do tipo exato de um parâmetro ou retorno.
Exemplo: um serviço espera List e outro passa Set, quebrando a integração.
Connascence of Value (CoV): dependência de valores específicos.
Exemplo: IDs ou códigos de status duplicados em múltiplos serviços; alterar o significado de um valor em um lugar exige alteração em todos os outros.
Connascence of Execution/Timing (CoE/CoT): dependência da ordem de execução.
Exemplo: um produtor Kafka que não garante a ordem esperada de eventos pode causar inconsistências em serviços consumidores.
A orientação geral é:
Tentar manter connascence forte (por exemplo, dependência de valores e ordem de execução) confinada dentro de um módulo.
Expor nas fronteiras do sistema dependências mais fracas e estáveis, como tipos bem definidos (contratos) e nomes claros.
Um exemplo simples em Java:
// Connascence de valor: o ID é apenas um número que precisa bater com outro serviço
public class Pedido {
private Long clienteId; // precisa "bater" com o ID usado em ClienteService
}
// Melhora: relação mais explícita
@Entity
public class Pedido {
@ManyToOne
private Cliente cliente; // referencia forte, reduz Connascence de valor entre serviços locais
}
Em sistemas distribuídos, técnicas como schemas de eventos (Avro, JSON Schema) e correlação por IDs bem definidos ajudam a controlar connascence entre serviços. Por exemplo, um correlationId compartilhado em uma saga facilita rastrear o fluxo sem obrigar cada serviço a conhecer detalhes internos das entidades dos outros.
Refatoração prática rumo à modularização
Aplicando coesão, acoplamento e connascence em um projeto real (por exemplo, um monolito Spring Boot), um caminho prático é:
Medir e observar:
- Use ferramentas de análise estática para inspecionar coesão (LCOM) e acoplamento (Ca, Ce) de pacotes e módulos.
- Identifique classes com LCOM alto e módulos com Ca/Ce desequilibrados.
Extrair fronteiras naturais:
- Use conceitos de Domain-Driven Design (DDD) para encontrar bounded contexts (Pedidos, Pagamentos, Catálogo, Clientes).
- Reorganize pacotes e módulos para refletir essas fronteiras de domínio.
*Reduzir acoplamento concreto: *
- Introduza interfaces em pontos de integração (notificação, gateways de pagamento, repositórios).
- Use injeção de dependência para depender de abstrações, não de implementações.
Controlar connascence entre serviços:
- Use contratos explícitos (APIs, schemas de eventos) e evite compartilhamento implícito de valores “mágicos”. - Mantenha regras mais frágeis (valores específicos, ordem de eventos) encapsuladas dentro de um serviço.
Evoluir para microsserviços (quando fizer sentido):
- A partir de um monolito modular bem definido, alguns módulos de alta coesão e responsabilidade clara podem ser extraídos como serviços independentes (por exemplo, PedidoService, PagamentoService, NotificacaoService).
- A comunicação entre serviços tende a migrar de chamadas diretas para eventos assíncronos, reduzindo acoplamento temporal e facilitando deploys e escalabilidade independentes.
Essa abordagem permite que o sistema evolua sem grandes reescritas: mudanças ficam mais localizadas, a reutilização aumenta e o impacto de bugs diminui.
Referências:
Robert C. Martin – Agile Software Development: Principles, Patterns, and Practices.
Robert C. Martin – Clean Architecture.
Meilir Page-Jones – What Every Programmer Should Know About Object-Oriented Design.
Java Platform Module System.
Top comments (0)