Skip to main content

Command Palette

Search for a command to run...

Modularidade

Métricas para avaliar qualidade de código

Updated
10 min read

💡
Série de artigos que vou escrever estudando o livro Software Architecture Fundamentals documentando meu aprendizado e minha visão dos capítulos.

Quem é desenvolvedor já deve ter trabalhado em sistemas desorganizados; em sistemas novos que começaram organizados e que acabou se tornando desorganizado ao longo do tempo.

Você já ficou na dúvida de onde colocar alguma classe, função e colocou num pacote chamado "utils".

Tudo isso afeta a qualidade final da arquitetura e do código como um todo.

Quanto mais complexo um sistema, mais esforço deve ser feito para manter uma organização. Um sistema nesse sentido é parecido com uma casa, é necessário dar manutenção senão ela desaba.

Nosso cérebro naturalmente organiza as coisas em blocos, na arquitetura essa organização do código se dá por módulos, e aqui cabe uma definição de módulo que abriga o mesmo conceito independente da linguagem utilizada.

Modularidade é o agrupamento lógico de códigos relacionados.

Para discussões de arquitetura, usamos modularidade como um termo geral para grupos de códigos relacionados: classes, funções, ou qualquer outro tipo de agrupamento.

Modularidade não requer uma separação física. Por exemplo, é possível ter um monolito com suas classes no mesmo projeto, mas com uma separação lógica. Se chegar o momento que seja necessário separar o código em alguns microservicos, o esforço seria menor. O porém é que separação lógica e não física requer mais maturidade da equipe de não misturar os conceitos, quando tem essa barreira física, é mais dificil misturar.

Faz parte do papel do Arquiteto e do desenvolvedor de gastar "energia" para garantir a manutenção saudável do sistema. E essa organização do código, é uma característica implicita de arquitetura, nenhum cliente passa o requisito específico de manter o código organizado (manutenabilidade).

Um arquiteto deve estar ciente de como os desenvolvedores organizam as coisas, porque isso tem implicações importantes na arquitetura. Se os pacotes estão muito acoplados, reutilizá-los para trabalhos relacionados fica mais difícil.

Eu considero também que muitas vezes a organização dos pacotes pelos desenvolvedores, demonstra uma falta de conhecimento da equipe de como a arquitetura funciona e também temos os problemas com a modelagem de dados.

Medindo Modularidade

Temos três conceitos fundamentais para medir modularidade de um sistema, coesão, acoplamento e connascence (sem tradução para o português). E temos métricas para medí-los.

Aqui cabe um aviso, eu coloquei os exemplos nos conceitos que foram mais complicados para entender.

Coesão

Coesão mede o quanto as partes são relacionadas entre si. O quanto os indivíduos daquele grupo fazem realmente parte daquele grupo.

Idealmente você deve agrupar partes que são inerentemente dependentes, tanto que se você quebrar em módulos menores, você ainda vai ter que relacionar eles por chamadas.

Cientistas da computação definiram um grupo de medidas de coesão, listado do melhor para o pior.

Tipos de coesão.

  1. Coesão funcional

    Cada parte do módulo está relacionada entre si e o módulo contém tudo que é essencial para funcionar. Esse é o mundo perfeito da coesão.

  2. Coesão sequencial

    Dois módulos interagem, onde a saida de um se torna a entrada do outro.

    Pegando um exemplo simples, programação funcional tem muita coesão sequencial. No exemplo abaixo, o select só vai pegar os dados das pessoas filtradas pelo Where.

       var pessoas = new List<Pessoa>() {new(10, "Jose"), new(18, "Maria")};
    
       return pessoas.Where(p => p.Idade >= 18).Select(p => p.Nome);
    
  3. Coesão comunicacional

    Demorei um pouco para formular na minha cabeça sobre esse tipo de coesão, a chave para entender são os dados. Os dados são compartilhados entre os elementos do módulo ou entre os módulos, gerando uma relação entre eles. A fonte de informação é a mesma, porém as tarefas a serem executadas podem ser diferentes.

    No livro eles fazem um exemplo com salvar dados no banco e enviar um email. Abaixo um exemplo em C# onde o ponto de coesão são os valores (data) utilizados nos dois métodos distintos.

     using System;
     using System.Linq;
    
     class Program
     {
         static void Main()
         {
             // Exemplo de dados
             int[] data = { 10, 5, 8, 15, 7 };
    
             // Chamando funções do módulo estatístico
             double average = CalculateAverage(data);
             int sum = CalculateSum(data);
    
             // Exibindo resultados
             Console.WriteLine($"Média: {average}");
             Console.WriteLine($"Soma: {sum}");
         }
    
         // Função para calcular a média
         static double CalculateAverage(int[] values)
         {
             // A coesão comunicacional acontece através do parâmetro 'values'
             double average = values.Average();
             return average;
         }
    
         // Função para calcular a soma
         static int CalculateSum(int[] values)
         {
             // A coesão comunicacional acontece através do parâmetro 'values'
             int sum = values.Sum();
             return sum;
         }
     }
    
  4. Coesão procedural

    Dois módulos devem executar o código numa ordem particular. Como no exemplo abaixo a função "CalculateStatistics" funciona de maneira procedural, onde cada passo é necessario ser realizado em ordem para ter o resultado.

     using System;
     using System.Linq;
    
     class Program
     {
         static void Main()
         {
             // Exemplo de lista de números
             int[] numbers = { 5, 8, 2, 10, 7 };
    
             // Chamando funções do módulo de manipulação de lista
             CalculateStatistics(numbers);
         }
    
         // Função procedural para calcular média e desvio padrão
         static void CalculateStatistics(int[] values)
         {
             // Calcula a média
             double average = values.Average();
             Console.WriteLine($"A média dos números é: {average:F2}");
    
             // Calcula o desvio padrão
             double sumOfSquares = values.Sum(v => 
                 Math.Pow(v - average, 2));
             double variance = sumOfSquares / values.Length;
             double standardDeviation = Math.Sqrt(variance);
             Console.WriteLine($"O desvio padrão dos números é: 
                 {standardDeviation:F2}");
         }
     }
    
  5. Coesão temporal

    Módulos estão relacionados com base no tempo. Eles não tem relação entre eles a não ser pela característica temporal. Por exemplo, o Startup de uma aplicação .NET.

     var builder = WebApplication.CreateBuilder(args);
    
     // Add services to the container.
     builder.Services.AddControllers();
    
     builder.Services.AddEndpointsApiExplorer();
     builder.Services.AddSwaggerGen();
    
     var configuration = builder.Configuration;
    
     var connectionString = configuration.GetSection(
     "ConnectionStrings:DefaultConnection");
    
     builder.Services.AddHealthChecks(configuration);
    
  6. Coesão lógica

    Os dados dentro do módulo são relacionados lógicamente, mas não funcionalmente. O pacote StringUtils do Java, são métodos que atuam em cima da String, mas fora isso não tem relação um com outro.

  7. Coesão coincidente.

    Elementos do módulo não são relacionados a não ser estar na mesma pasta. (Pior cenário, o utils)

Como medir coesão?

LCOM, que significa "Lack of Cohesion of Methods," é uma métrica usada para avaliar a coesão em uma classe em programação orientada a objetos. Coesão refere-se à medida em que os membros de uma classe (métodos e campos) estão relacionados uns com os outros. Uma coesão alta indica que os membros estão fortemente relacionados e trabalham juntos para alcançar um propósito comum. Por outro lado, uma coesão baixa sugere que os membros da classe podem não estar tão relacionados e podem ter responsabilidades divergentes.

A métrica LCOM é uma maneira de quantificar a coesão em uma classe, com o objetivo de avaliar a qualidade do design do software.

A ideia é contar quantos pares de métodos não compartilham campos em comum e, em seguida, calcular a porcentagem disso em relação ao número total de pares de métodos possíveis.

É importante notar que a métrica LCOM é uma ferramenta e não deve ser usada isoladamente para avaliar a qualidade de um design. Ela fornece insights sobre a coesão, mas o contexto e a compreensão mais profunda do código são essenciais para uma avaliação completa. Além disso, existem outras métricas de coesão e acoplamento que podem ser usadas em conjunto para fornecer uma visão mais abrangente da qualidade do design do software.

Acoplamento

Acoplamento refere-se ao grau de interdependência entre módulos de um sistema.

Assim como coesão ele é dividido em diferentes categorias da melhor para pior.

  1. Dados

    Acoplamento de dados ocorre quando os módulos compartilham dados através de parametros, mas não tem conhecimento da implementação interna um do outro. Além de que um dado nao altera o dado do outro módulo.

  2. Estrutura de Dados

    Em inglês é Stamp, algo como carimbo, mas eu achei melhor colocar Estrutura de dados porque ele está relacionado com os módulos compartilharem estruturas completas de dados não apenas dados.

  3. Controle

    Um módulo exerce controle no comportamento de outro módulo. Isso indica que um módulo possui conhecimento interno do outro módulo.

  4. Externo

    O quanto um sistema depende de entidades externas aos módulos que estão sendo desenvolvidos. Dependencia de banco, de arquivos.

  5. Comum

    Os módulos compartilham de um dado global. Então alterações nesses dados podem afetar todos os módulos.

  6. Conteúdo

    Quando dois ou mais módulos compartilham código. Um afeta diretamente a funcionalidade do outro. Esse aqui me confundiu um pouco. Eu entendo como uma falha de implementação em modificadores de acesso no caso do C#.

Como medir acoplamento?

Edward Yourdon e Larry Constantine definiram duas métricas de acoplamento. A Aferente e a eferente.

Aferente mede o número de conexões chegando (incoming) para um artefato de código.

Eferente mede o número de conexões saindo (outgoing) para outros artefatos de código.

No geral os tipos de conexões podem variar se são piores ou melhores. Para o quesito acoplamento, conexões aferentes (que estão chegando) são piores que as eferentes, porque significa que alterações em outros módulos podem quebrar o seu módulo e isso tem haver com métricas criadas pelo Robert Martin que explico a seguir.

Abstração, instabilidade e distância da sequencia principal.

Essas métricas foram criadas pelo Robert Martin para uma linguagem específica (C++), mas pode ser utilizado em outras linguagens.

Abstração

Abstração é a razão de artefatos abstratos (classes abstratas, interfaces) para artefatos concretos (implementação).

Existe um extremo que é sem nenhuma abstração, o código está todo no "main" e no outro extremo muita abstração pode se tornar mais complicado para o desenvolvedor de entender como as coisas se conectam.

A abstração é calculada pela razão da soma dos artefatos abstratos (interfaces e classes abstratas) com a soma das classes concretas.

$$A = \sum m^a \div \sum m^c$$

Instabilidade

É a razão entre acoplamento eferente com a soma de eferente + aferente.

A métrica de instabilidade determina a volatilidade do código fonte. Um código fonte que possui alta porcentagem de instabilidade quebra mais facilmente quando modificado devido a alto acoplamento.

É só pensar essa razão leva em conta o quanto o seu sistema depende de chamadas externas. Então quanto maior for e quanto mais facilmente seu sistema é quebrado por mudanças de fora.

A instabilidade é calculada pela soma dos acoplamentos eferentes (conexões que saem) dividido pela soma dos acoplamentos eferentes e aferentes.

$$I = C^e \div C^e + C^a$$

Distância da Sequencia principal

Essa distância é calculada pelo módulo da soma de "abstração" e "instabilidade" menos 1.

$$D = |A + I - 1 |$$

Essa sequencia principal define a relação ideal entre abstração e instabilidade. Sair dessa linha indica algum problema no seu módulo.

Connascence

Dois componentes são connascents se uma mudança em um exige uma mudança no outro para manter o sistema correto. - Meilir Page-Jones.

Existem dois tipos de connascence: Estática e Dinâmica.

Como os autores explicam, é um refinamento do acoplamento aferente e eferente. O arquiteto consegue ver pelos tipos de connascence o grau de acoplamento seja aferente ou eferente.

  1. Connascence de Nome

    Componentes devem concordar com o nome de uma entidade. É o nível mais fraco de connasnce, porque mudar é facil, não gera tanto impacto.

  2. Connascence de Tipo

    Componentes devem concordar com o tipo de uma entidade.

  3. Connascence de Significado (Convenção)

    Componentes devem concordar com o significado de certos valores particulares. Exemplo clássico é o Boolean que é tratado em muitas linguagens como TRUE = 1 e FALSE = 0.

  4. Connascence de Posição

    Componentes devem concordar com a ordem dos valores. Esse aqui é um exemplo muito comum quando o método tem como parâmetros por exemplo duas strings.

     void CreateCategory(String name, String description);
    

    Se o desenvolvedor trocar a ordem dos valores não vai gerar erro, mas semanticamente está errado.

  5. Connascence de Algortimo

    Componentes devem concordar com um algoritmo. Um exemplo é o algoritmo de hash que deve rodar no cliente e no servidor devem ser o mesmo para autenticar um usuário, se um dos lados alterar o algoritmo, quebra o outro componente.

  6. Connascence de Execução

    A ordem de execução dos componentes é importante.

  7. Connascence de Timing

    O "timing" de execução de multiplos componentes é importante.

  8. Connascence de Valor

    Ocorre quando vários valores relacionam entre si e devem mudar juntos. Pensando em uma estrutura de microserviço você tem seus diferentes contextos e compartilha algumas informações, quando um valor é alterado em um microserviço tem que ser atualizado no resto.

  9. Connascence de Identidade

    Ocorre quando múltiplos componentes devem referencia a mesma entidade.

    Componentes independentes compartilham uma estrutura de dados em comum.