Dois truques mágicos para manter uma base de código saudável

21 de outubro de 2019

Com apenas duas restrições automatizáveis, podemos conseguir desdobramentos muito interessantes para nos ajudar a manter a saúde de uma base de código.

A arte de programar consiste em organizar e dominar a complexidade

(Edsger Dijkstra)

1) Não deixe a cobertura de testes cair

O ponto-chave é não deixar o projeto deteriorar. Se ele está no começo, é uma chance de ouro para começar com mais rigor e vê-lo crescer estável, legível e bem testado. Tão ou mais importante do que ter uma boa cobertura percentual de testes é impedir que ela recue. Não tenha medo de exigir 100% de cobertura no código que está para ser incorporado na master ou em uma branch. Se essa exigência for feita desde o início do projeto, veja que maravilha: você terá 100% de cobertura de testes unitários.

Aliás, não se limite a exigir uma cobertura total. Use esse momento para preparar toda a esteira de entrega, do commit ao ambiente de produção. Farei um novo texto sobre isso em breve.

A pirâmide de testes de Mike Cohn

A pirâmide de testes, de Mike Cohn. A base, que queremos alargar, é composta pelos testes unitários.
Crédito da imagem: Martin Fowler

Motivações

  • Aumentar a cobertura de código quando a base estiver grande é mais penoso
  • Testes não nos livram integralmente de . Contudo, em qual cenário é mais provável topar com mais problemas: uma base de código com testes ou sem?
  • Mesmo que o time não pratique os testes primeiro (TDD), escrever um grande volume de testes depois de fazer o código é muito sofrido. O build, muito provavelmente, vai quebrar. Isso condiciona as pessoas a fazer commits menores e, por conta disso, dividir as tarefas em partes menores.
  • Tarefas mais atômicas proporcionam uma melhor visibilidade do andamento do projeto e oportunidades de paralelismo entre os membros do time
  • Times muito paralelos, por sua vez, tendem a perceber mais rapidamente os incômodos do código, como classes que comecem a fazer mais coisas do que deveriam. Muitas pessoas mexendo em uma mesma classe normalmente é um sinal de que ela tem muitas dependências e/ou responsabilidades. Modificá-la fica cada vez mais difícil.

Como chegar lá

  • Tenha um processo automatizado de build (rodando no Jenkins, Drone, Travis ou que preferir).
  • Faça o build disparar a cada commit.
  • Utilize um plugin para calcular como ficará a cobertura de testes com o código que está sendo enviado no commit.
  • Use um plugin (como o Cobertura, no caso da dupla Java + Jenkins) que falhe o build caso as métricas estejam abaixo do limite.
  • O pulo do gato é atualizar os limites a cada build: se a cobertura cresceu de 40% para 44%, então essa passa a ser a cobertura mínima para o que o build passe (o Cobertura faz isso). Se, num commit seguinte, o percentual tiver caído, o build precisa falhar. Esse é o coração do negócio.
  • Alternativamente, use o SonarQube. Ele permite gerar métricas analisando apenas o código que está entrando no commit. Ele é um analisador genérico e com suporte a várias linguagens, por isso o recomendo sem cerimônia. Em seus quality gates, procure a métrica “Coverage on new code”, sapeque 100% como exigência e associe-a ao quality profile do seu projeto. Por fim, vincule o sucesso do build ao quality gate.

Os resultados que vi foram bem interessantes:

  • Códigos desnecessários não são cobertos pelos testes, o que faz build quebrar. As pessoas pararam de projetar canhões para matar moscas.
  • As tarefas passaram a ser mais subdivididas
  • O planejamento e a organização do time ficaram mais apurados
  • As entregas ficaram distribuídas mais homogeneamente ao longo do tempo

2) Mantenha as classes com uma única responsabilidade

Foto em que se vê um enorme kaiju atrás de prédios de três andares. O monstro é três vezes mais alto do que os prédios

Uma classe kaiju, aquela que todo mundo tem medo de mexer

O que desejamos é escrever código que pareça óbvio. Se alguém vir seu trabalho e não demorar a falar um “ah, beleza!”, pode ficar feliz: você está no caminho certo.

Em programação dinâmica, existe um conceito chamado Princípio da Otimalidade (Richard Bellman, 1957):

Para um dado estado do sistema, a política ótima para os estados remanescentes é independente da política de decisão adotada em estados anteriores.

Em outras palavras, uma solução é a melhor possível - a solução ótima - se cada uma de suas subdivisões é também a melhor possível. Por exemplo: um pacote TCP chegará a seu destino pela melhor rota se, a cada passo, o roteador que tem o pacote escolher como roteador seguinte aquele que proporcionar o menor tempo de transferência dos dados.

Aplicando esse conceito aos nossos projetos, o que temos é que eles serão bacanas, enxutos e fáceis de compreender se cada uma de suas partes também o forem. A classe é nossa unidade fundamental de trabalho. Se não abrirmos mão de construir classes enxutas e coesas, assim também serão os agrupamentos de mais alto nível, como pacotes, módulos, bibliotecas etc.

Motivações

  • Faz parte do nosso trabalho dividir problemas grandes em partes menores. Dividir uma classe kaiju em vários calangos especialistas segue o mesmo raciocínio.
  • Testar classes grandes dá um trabalho enorme, sobretudo se você escrever os testes depois do código real.
  • Quando as classes se relacionam com muitas outras e/ou seus métodos têm muitos parâmetros, temos aí uma explosão combinatória de entradas, saídas e estados. Isso torna o código muito mais vulnerável a bugs, já que é difícil mapear todas as possibilidades.
  • Compaixão com seu eu daqui a 6 meses, quando você nem se lembrar mais o que esse código faz.

Como chegar lá

  • Veja o código de fora para dentro, pensando NO QUE ele faz, e não COMO, e responda a pergunta: o que essa classe deveria fazer?
  • Se sua resposta tiver as palavras E ou OU, é porque existe mais de uma responsabilidade. Descreva essa responsabilidade por meio de uma nova classe (testes e nomes explicativos são ótimas maneiras de comunicar as intenções). Responda a pergunta para a nova classe, e assim recursivamente.
  • Use ferramentas de análise estática, como Lint e Sonar, para analisar o código. A partir de características como da quantidade de linhas, número de métodos e quantidade de relacionamentos com outras classes, essas ferramentas conseguem identificar se sua classe está virando um canivete suíço.
  • Não se limite a meramente obter essas métricas. Use-as como pontos decisivos em seu processo de build. Se as métricas estiverem desfavoráveis em relação aos limites definidos por você, o build precisa falhar.
  • Não paralise os trabalhos do seu time. Se você ativar essa quebra de build em um projeto em andamento, configure os limites dos indicadores de qualidade de acordo com o estado atual que foi medido no projeto. Se hoje ele tem 4 kaijus e 15 megazords, assim seja.
  • Se sua ferramenta de análise tiver a capacidade de inspecionar apenas o código que está entrando com o commit (como o SonarQube), aí sim, você pode ser radical: megazords recém saídos de fábrica não podem entrar e ponto final.

Conclusões

  • O rigor com o código que está entrando na base é interessante para permitir que o passivo técnico aumente. Se o projeto for iniciado já com essas barreiras, as chances de ele evoluir de maneira saudável são muito boas.
  • As medidas também valem para projetos em andamento. Se eles já têm seu passivo técnico, paciência, mas ele ao menos não vai aumentar.
  • O início da adoção pode ser doloroso e gerar ruídos. Contudo, com o tempo, o time se adapta e divide o trabalho em partes menores para que os ajustes sejam mais fáceis caso o código seja reprovado no processo de build.
  • Por melhores que eventualmente sejam as ferramentas, as práticas só funcionam se forem abraçadas pelas pessoas. Não é apenas o ferramental que nos ajuda, mas o entendimento de que nós, como humanos, eventualmente nos distraímos e erramos. As ferramentas estão lá para nos apontar os problemas quando deixarmos o cachimbo cair.

Para ler mais

Bônus:

Compartilhar: