Fundamentos da Arquitetura de Software - Capitulo 3
Há muito tempo arquitetos e desenvolvedores lidam com o conceito de modularidade. Como ficou evidente no clássico Composite/Structured Design (1978): "A modularidade é um princípio organizador".
Desvendando a Modularidade: O Coração da Arquitetura de Software
A modularidade não é apenas uma palavra da moda; é a base que sustenta sistemas manuteníveis e escaláveis. Como desenvolvedores, passamos grande parte do tempo tentando equilibrar a divisão de responsabilidades sem cair na armadilha da complexidade desnecessária.
Modularidade vs. Granularidade
Embora frequentemente confundidos, são conceitos distintos:
. Modularidade: Trata da divisão de sistemas em partes menores (como a transição de um estilo de arquitetura monolítica para microsserviços).
. Granularidade: Está relacionada com o tamanho dessas partes.
"Adote a modularidade, mas cuidado com a granularidade." — Mark Richards
O segredo está no equilíbrio. A granularidade excessiva faz os serviços ou componentes ficarem acoplados uns aos outros, criando antipadrões arquiteturais complexos e difíceis de manter, como a Arquitetura Espaguete, os monólitos distribuídos e a famosa "bola de lama distribuída".
Para evitar esses cenários, pesquisadores desenvolveram métricas agnósticas de linguagem para medir a modularidade. Vamos focar em três pilares essenciais: Coesão, Acoplamento e Conascência.
1. Coesão (O que está dentro do módulo)
A coesão mede a extensão até a qual as partes de um módulo devem estar contidas dentro do mesmo módulo. Em resumo, mede quantas partes estão relacionadas umas com as outras.
"Tentar dividir um módulo coeso resultaria apenas em aumento de acoplamento e diminuição da legibilidade." — Structured Design
Os cientistas da computação definiram níveis de coesão, do melhor para o pior:
1. Coesão Funcional (Ideal): Tudo no módulo é essencial para seu funcionamento.
2. Coesão Sequencial: A saída de uma parte é a entrada da outra.
3. Coesão Comunicacional: Partes operam com as mesmas informações.
4. Coesão Procedural: Código deve ser executado em uma ordem específica.
5. Coesão Temporal: Relação baseada em dependências de tempo.
6. Coesão Lógica: Dados relacionados logicamente, mas não funcionalmente.
7. Coesão Coincidente (Pior caso): Elementos sem relação entre si, agrupados no mesmo arquivo.
Para medir isso estruturalmente, usamos o LCOM (Falta de coesão em métodos), que expõe o acoplamento acidental dentro das classes.
Entendendo o LCOM na Prática (Métricas de Coesão)
A métrica LCOM mede a "falta de coesão". Portanto, quanto maior o LCOM, pior é o seu código.
De forma simplificada, o LCOM analisa as variáveis de instância (atributos) de uma classe e verifica quantos métodos as utilizam. Se você tem um grupo de métodos que usa apenas o atributo A, e outro grupo de métodos que usa apenas o atributo B, você tem "ilhas" isoladas dentro da mesma classe. O LCOM expõe exatamente esse acoplamento acidental.
Vamos ver um exemplo clássico de um LCOM alto (ruim) e como refatorá-lo.
❌ Exemplo de Alto LCOM
Imagine uma classe UsuarioService. Com o tempo, ela foi crescendo e assumindo responsabilidades demais.
public class UsuarioService {
// Grupo de atributos 1: Perfil
private String nome;
private String email;
// Grupo de atributos 2: Faturamento
private String cartaoCredito;
private BigDecimal saldo;
// --- MÉTODOS DA ILHA 1 (Perfil) ---
public void atualizarPerfil(String novoNome, String novoEmail) {
this.nome = novoNome;
this.email = novoEmail;
System.out.println("Perfil atualizado!");
}
public String obterResumoPerfil() {
return nome + " (" + email + ")";
}
// --- MÉTODOS DA ILHA 2 (Faturamento) ---
public void processarCobranca(BigDecimal valor) {
if (this.saldo.compareTo(valor) >= 0) {
this.saldo = this.saldo.subtract(valor);
System.out.println("Cobrança no cartão " + cartaoCredito + " processada.");
}
}
public void adicionarSaldo(BigDecimal valor) {
this.saldo = this.saldo.add(valor);
}
}
Análise do LCOM neste código: Se você rodar uma ferramenta de análise estática (como SonarQube ou dependometer) nesta classe, o LCOM vai estourar. Por quê? Porque os métodos de Perfil (atualizarPerfil, obterResumoPerfil) não interagem com os atributos de Faturamento (cartaoCredito, saldo), e vice-versa. Temos duas classes distintas do mesmo arquivo, unidas apenas por uma "Coesão Coincidente".
✅ Exemplo de Baixo LCOM (Classes Coesas)
Para zerar (ou minimizar) o LCOM, a solução é aplicar o Princípio de Responsabilidade Única (SRP) e quebrar a classe em componentes menores e altamente coesos, onde os métodos realmente precisem de todos (ou quase todos) os atributos da classe.
// Classe 1: Altamente coesa e focada apenas no perfil
public class PerfilUsuarioService {
private String nome;
private String email;
public void atualizarPerfil(String novoNome, String novoEmail) {
this.nome = novoNome;
this.email = novoEmail;
}
public String obterResumoPerfil() {
return nome + " (" + email + ")";
}
}
// Classe 2: Altamente coesa e focada apenas no aspecto financeiro
public class FaturamentoUsuarioService {
private String cartaoCredito;
private BigDecimal saldo;
public void processarCobranca(BigDecimal valor) {
if (this.saldo.compareTo(valor) >= 0) {
this.saldo = this.saldo.subtract(valor);
}
}
public void adicionarSaldo(BigDecimal valor) {
this.saldo = this.saldo.add(valor);
}
}
Por que isso é melhor? Agora, se houver um bug na lógica de cobrança, você não corre o menor risco de quebrar a lógica de atualização de perfil. O LCOM de ambas as classes despencou para o nível ideal, pois todos os métodos trabalham ativamente com os atributos declarados em suas respectivas classes.
Coesão no Java
Um módulo com baixa coesão tenta abraçar o mundo. A alta coesão foca em uma única responsabilidade.
❌ Baixa Coesão (Classe "Faz Tudo")
Essa classe agrupa métodos que não têm relação funcional entre si. Isso fere o Princípio de Responsabilidade Única (SRP).
public class Utilidades {
public void salvarUsuario(Usuario u) { /* Acesso ao banco */ }
public void calcularImposto(Pedido p) { /* Regra de negócio financeira */ }
public void enviarEmail(String email, String msg) { /* Integração externa */ }
}
✅ Alta Coesão (Coesão Funcional)
Aqui, a classe tem um propósito claro e todas as suas partes trabalham para atingir esse objetivo.
public class CalculadoraDeImpostos {
public BigDecimal calcularImpostoNacional(Pedido p) { ... }
public BigDecimal calcularImpostoInternacional(Pedido p) { ... }
}
2. Acoplamento (As conexões externas)
O acoplamento analisa como seu código se conecta com o resto do sistema e é dividido em dois tipos:
. Aferente (Ca): Mede o número de conexões de entrada (quem chama o seu código).
. Eferente (Ce): Mede as conexões de saída (quem o seu código chama).
A partir disso, derivamos duas métricas essenciais:
. Abstração: A proporção entre artefatos abstratos (interfaces/classes abstratas) e concretos.
. Instabilidade: A razão entre o acoplamento eferente e o acoplamento total (Ce / (Ce + Ca)). Determina a volatilidade do seu codebase. Código muito instável quebra facilmente quando o entorno muda.
Reduzindo Acoplamento Eferente
❌ Alto Acoplamento (Dependência Rígida)
A classe PedidoService está fortemente acoplada à implementação concreta EnvioCorreios.
public class PedidoService {
// Acoplamento forte com uma implementação concreta
private EnvioCorreios correios = new EnvioCorreios();
public void processar(Pedido pedido) {
// ... regras de negócio
correios.enviar(pedido);
}
}
✅ Baixo Acoplamento (Inversão de Dependência - SOLID)
Dependendo de uma interface (abstração), o acoplamento diminui. A classe não sabe mais como o envio é feito, apenas que ele existe.
public class PedidoService {
private final ServicoDeEnvio servicoDeEnvio; // Abstração
// A dependência é injetada (pelo Spring, por exemplo)
public PedidoService(ServicoDeEnvio servicoDeEnvio) {
this.servicoDeEnvio = servicoDeEnvio;
}
public void processar(Pedido pedido) {
// ... regras de negócio
servicoDeEnvio.enviar(pedido);
}
}
A Distância da Sequência Principal
Essa métrica sugere uma relação ideal entre abstração e instabilidade. Classes que ficam perto dessa linha exibem uma combinação saudável.
3. Conascência (A linguagem refinada do acoplamento)
A programação orientada a objetos introduziu a Conascência, um termo mais preciso para descrever o acoplamento. Dois componentes são conascentes se uma alteração em um exigir que o outro seja modificado para manter o sistema funcionando.
Ela se divide em:
. Estática (Nível de código): Nome, Tipo, Significado, Posição e Algoritmo.
. Dinâmica (Tempo de execução): Execução (ordem), Timing, Valores e Identidade.
Ao analisar a conascência, os arquitetos avaliam três propriedades:
1. Força: Quão fácil é refatorar esse acoplamento?
2. Localidade: Os módulos estão próximos ou espalhados pelo codebase?
3. Grau: Qual o tamanho do impacto de uma alteração?
Melhorando a Conascência Estática
A Conascência de Posição é fraca (ruim) e comum em assinaturas de métodos longas, onde a ordem dos parâmetros importa e pode causar bugs silenciosos.
❌ Conascência de Posição (Fraca/Ruim)
public void registrarCliente(String nome, String email, String cpf, String telefone) { ... }
// Chamada do método (Oops! Inverti o email e o CPF sem o compilador reclamar)
registrarCliente("Alex", "000.000.000-00", "alex@email.com", "85999999999");
✅ Refatorando para Conascência de Nome/Tipo (Forte/Boa)
Passamos a depender de um tipo específico (como um Record). Agora, dependemos do nome e do tipo da estrutura, que é muito mais seguro de refatorar.
public record DadosCliente(String nome, String email, String cpf, String telefone) {}
public void registrarCliente(DadosCliente dados) { ... }
// Chamada do método (Seguro e claro, ordem dos argumentos na chamada original não gera ambiguidade de tipo)
DadosCliente novoCliente = new DadosCliente("Alex", "alex@email.com", "000.000.000-00", "85999999999");
registrarCliente(novoCliente);
Conclusão
Dominar a modularidade através da coesão, acoplamento e conascência é o que separa o ato de "apenas escrever código" do design de uma Arquitetura de Software robusta. No dia a dia, busque o equilíbrio: maximize a coesão, controle as dependências e fique atento à força das ligações entre seus componentes.