Posts
4 Práticas para Reduzir Sua Taxa de Defeitos
17 de novembro de 2015 • 9 min de leitura

Escrever software é uma batalha entre complexidade e simplicidade. Encontrar o equilíbrio entre os dois é difícil. O trade-off é entre métodos longos e não mantíveis e muita abstração. Inclinar-se demais em qualquer direção prejudica a legibilidade do código e aumenta a probabilidade de defeitos.
Os defeitos são evitáveis? A NASA tenta, mas eles também fazem carradas de testes. Seu software é literalmente crítico para a missão – uma única chance. Para a maioria das organizações, este não é o caso e grandes quantidades de testes são custosas e impraticáveis. Embora não haja substituto para testes, é possível escrever código resistente a defeitos, sem testes.
Em 20 anos codificando e arquitetando aplicações, identifiquei quatro práticas para reduzir defeitos. As duas primeiras práticas limitam a introdução de defeitos e as duas últimas práticas expõem defeitos. Cada prática é um tópico vasto por si só sobre o qual muitos livros foram escritos. Destilei cada prática em alguns parágrafos e forneci links para informações adicionais quando possível.
1. Escreva Código Simples
Simples deveria ser fácil, mas não é. Escrever código simples é difícil.
Alguns lerão isso e pensarão que isso significa usar recursos simples da linguagem, mas este não é o caso — código simples não é código burro.
Para manter a objetividade, estou usando complexidade ciclomática como medida. Existem outras maneiras de medir complexidade e outros tipos de complexidade, espero explorar esses tópicos em artigos futuros.
A Microsoft define complexidade ciclomática como:
A complexidade ciclomática mede o número de caminhos
linearmente independentes através do método, que é determinado
pelo número e complexidade de ramificações condicionais. Uma baixa
complexidade ciclomática geralmente indica um método que é fácil
de entender, testar e manter.
O que é uma baixa complexidade ciclomática? A Microsoft recomenda manter a complexidade ciclomática abaixo de 25.
Para ser honesto, achei a recomendação da Microsoft de complexidade ciclomática de 25 muito alta. Para manutenibilidade e complexidade, descobri que o tamanho ideal do método é entre 1 a 10 linhas com uma complexidade ciclomática entre 1 e 5.
Bill Wagner em Effective C#, Second Edition escreveu sobre tamanho de método:
Lembre-se de que traduzir seu código C# em código executável pela máquina é um processo de duas etapas. O compilador C# gera IL que é entregue em assemblies. O compilador JIT gera código de máquina para cada método (ou grupo de métodos, quando inlining está envolvido), conforme necessário. Funções pequenas tornam muito mais fácil para o compilador JIT amortizar esse custo. Funções pequenas também são mais propensas a serem candidatas para inlining. Não é apenas pequenez: Fluxo de controle mais simples importa tanto quanto. Menos ramificações de controle dentro de funções tornam mais fácil para o compilador JIT registrar variáveis. Não é apenas boa prática escrever código mais claro; é como você cria código mais eficiente em tempo de execução.
Para colocar a complexidade ciclomática em perspectiva, o seguinte método tem uma complexidade ciclomática de 12.
public string ComplexityOf12(int status)
{
var isTrue = true;
var myString = "Chuck";
if (isTrue)
{
if (isTrue)
{
myString = string.Empty;
isTrue = false;
for (var index = 0; index < 10; index++)
{
isTrue |= Convert.ToBoolean(new Random().Next());
}
if (status == 1 || status == 3)
{
switch (status)
{
case 3:
return "Bye";
case 1:
if (status % 1 == 0)
{
myString = "Super";
}
break;
}
return myString;
}
}
}
if (!isTrue)
{
myString = "New";
}
switch (status)
{
case 300:
myString = "3001";
break;
case 400:
myString = "4003";
break;
}
return myString;
}
Uma hipótese de complexidade geralmente aceita postula que existe uma correlação positiva entre complexidade e defeitos.
A linha anterior é um pouco complicada. Nos termos mais simples — manter o código simples reduz sua taxa de defeitos.
2. Escreva Código Testável
Estudos mostraram que escrever código testável, sem escrever os testes reais, reduz as incidências de defeitos. Isso é tão importante e profundo que precisa ser repetido: Escrever código testável, sem escrever os testes reais, reduz as incidências de defeitos.
Isso levanta a questão, o que é código testável?
Defino código testável como código que pode ser testado isoladamente. Isso significa que todas as dependências podem ser mockadas a partir de um teste. Um exemplo de dependência é uma consulta ao banco de dados. Em um teste, os dados são mockados (falsificados) e uma asserção do comportamento esperado é feita. Se a asserção for verdadeira, o teste passa, se não, falha.
Escrever código testável pode parecer difícil, mas, na verdade, é fácil quando seguindo a Inversão de Controle (Injeção de Dependência) e os princípios S.O.L.I.D. Você ficará surpreso com a facilidade e se perguntará por que demorou tanto para começar a escrever dessa forma.
3. Revisões de Código
Uma das práticas mais impactantes que uma equipe de desenvolvimento pode adotar são as revisões de código.
As Revisões de Código facilitam o compartilhamento de conhecimento entre desenvolvedores. Falando por experiência, discutir abertamente código com outros desenvolvedores teve o maior impacto nas minhas habilidades de escrita de código.
No livro Code Complete, por Steve McConnell, Steve fornece numerosos estudos de caso sobre os benefícios das revisões de código:
- Um estudo de uma organização na AT&T com mais de 200 pessoas relatou um aumento de 14% na produtividade e uma diminuição de 90% nos defeitos após a organização introduzir revisões.
- A Aetna Insurance Company encontrou 82% dos erros em um programa usando inspeções e conseguiu diminuir seus recursos de desenvolvimento em 20%.
- Em um grupo de 11 programas desenvolvidos pelo mesmo grupo de pessoas, os primeiros 5 foram desenvolvidos sem revisões. Os 6 restantes foram desenvolvidos com revisões. Após todos os programas serem lançados em produção, os primeiros 5 tiveram uma média de 4,5 erros por 100 linhas de código. Os 6 que foram inspecionados tiveram uma média de apenas 0,82 erros por 100. As revisões reduziram os erros em mais de 80%.
Se esses números não te convencem a adotar revisões de código, então você está destinado a derivar para um buraco negro enquanto canta Johnny Paycheck’s Take This Job and Shove It.
4. Testes Unitários
Admito, quando estou contra um prazo, testes são a primeira coisa a ser abandonada. Mas os benefícios dos testes não podem ser negados, como os seguintes estudos ilustram.
A Microsoft realizou um estudo sobre a Efetividade dos Testes Unitários. Eles descobriram que codificar a versão 2 (a versão 1 não tinha testes) com testes automatizados imediatamente reduziu defeitos em 20%, mas a um custo adicional de 30%.
Outro estudo analisou o Desenvolvimento Orientado por Testes (TDD). Eles observaram um aumento na qualidade do código, mais de duas vezes, comparado a projetos similares não usando TDD. Projetos TDD levaram em média 15% mais tempo para desenvolver. Um efeito colateral do TDD foi que os testes serviram como documentação para as bibliotecas e APIs.
Por último, em um estudo sobre Cobertura de Testes e Defeitos Pós-Verificação:
… Descobrimos que em ambos os projetos o aumento na cobertura de
testes está associado com diminuição em problemas reportados em campo
quando ajustado pelo número de mudanças pré-lançamento…
Um Exemplo
O seguinte código tem uma complexidade ciclomática de 4.
public void SendUserHadJoinedEmailToAdministrator(DataAccess.Database.Schema.dbo.Agency agency, User savedUser)
{
AgencySettingsRepository agencySettingsRepository = new AgencySettingsRepository();
var agencySettings = agencySettingsRepository.GetById(agency.Id);
if (agencySettings != null)
{
var newAuthAdmin = agencySettings.NewUserAuthorizationContact;
if (newAuthAdmin.IsNotNull())
{
EmailService emailService = new EmailService();
emailService.SendTemplate(new[] { newAuthAdmin.Email }, GroverConstants.EmailTemplate.NewUserAdminNotification, s =>
{
s.Add(new EmailToken { Token = "Domain", Value = _settings.Domain });
s.Add(new EmailToken
{
Token = "Subject",
Value =
string.Format("New User {0} has joined {1} on myGrover.", savedUser.FullName(), agency.Name)
});
s.Add(new EmailToken { Token = "Name", Value = savedUser.FullName() });
return s;
});
}
}
}
Vamos examinar a testabilidade do código acima.
Este é código simples?
Sim, é, a complexidade ciclomática está abaixo de 5.
Existem dependências?
Sim. Existem 2 serviços AgencySettingsRepository
e EmailService
.
Os serviços são mockáveis?
Não, sua criação está oculta dentro do método.
O código é testável?
Não, este código não é testável porque não podemos mockar AgencySettingsRepository
e EmailService
.
Exemplo de Código Refatorado
Como podemos tornar este código testável?
Injetamos (usando injeção por construtor) AgencySettingsRepository
e EmailService
como dependências. Isso nos permite mocká-los a partir de um teste e testar isoladamente.
Abaixo está a versão refatorada.
Note como os serviços são injetados no construtor. Isso nos permite controlar qual implementação é passada para o construtor SendMail
. É então fácil passar dados fictícios e interceptar as chamadas dos métodos de serviço.
public class SendEmail
{
private IAgencySettingsRepository _agencySettingsRepository;
private IEmailService _emailService;
public SendEmail(IAgencySettingsRepository agencySettingsRepository, IEmailService emailService)
{
_agencySettingsRepository = agencySettingsRepository;
_emailService = emailService;
}
public void SendUserHadJoinedEmailToAdministrator(DataAccess.Database.Schema.dbo.Agency agency, User savedUser)
{
var agencySettings = _agencySettingsRepository.GetById(agency.Id);
if (agencySettings != null)
{
var newAuthAdmin = agencySettings.NewUserAuthorizationContact;
if (newAuthAdmin.IsNotNull())
{
_emailService.SendTemplate(new[] { newAuthAdmin.Email },
GroverConstants.EmailTemplate.NewUserAdminNotification, s =>
{
s.Add(new EmailToken { Token = "Domain", Value = _settings.Domain });
s.Add(new EmailToken
{
Token = "Subject",
Value = string.Format("New User {0} has joined {1} on myGrover.", savedUser.FullName(), agency.Name)
});
s.Add(new EmailToken { Token = "Name", Value = savedUser.FullName() });
return s;
});
}
}
}
}
Exemplo de Teste
Abaixo está um exemplo de teste isolado. Estamos usando o framework de mock FakeItEasy.
[Test]
public void TestEmailService()
{
//Given
//Using FakeItEasy mocking framework
var repository = A<IAgencySettingsRepository>.Fake();
var service = A<IEmailService>.Fake();
var agency = new Agency { Name = "Acme Inc." };
var user = new User { FirstName = "Chuck", LastName = "Conway", Email = "chuck.conway@fakedomain.com" }
//When
var sendEmail = new SendEmail(repository, service);
sendEmail.SendUserHadJoinedEmailToAdministrator(agency, user);
//Then
//An exception is thrown when this is not called.
A.CallTo(() => service.SendTemplate(A<Agency>.Ignore, A<User>.Ignore)).MustHaveHappened();
}
Conclusão
Escrever código resistente a defeitos é surpreendentemente fácil. Não me entenda mal, você nunca escreverá código livre de defeitos (se descobrir como, me avise!), mas seguindo as 4 práticas descritas neste artigo você verá uma diminuição nos defeitos encontrados no seu código.
Para recapitular, Escrever Código Simples é manter a complexidade ciclomática em torno de 5 e o tamanho do método pequeno. Escrever Código Testável é facilmente alcançado quando seguindo a Inversão de Controle e os Princípios S.O.L.I.D. Revisões de Código ajudam você e a equipe a entender o domínio e o código que você escreveu — apenas ter que explicar seu código revelará problemas. E por último, Testes Unitários podem melhorar drasticamente a qualidade do seu código e fornecer documentação para futuros desenvolvedores.
Autor: Chuck Conway é especialista em engenharia de software e IA Generativa. Conecte-se com ele nas redes sociais: X (@chuckconway) ou visite-o no YouTube.