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 equilíbrio entre os dois é difícil. A compensação é 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 também faz toneladas de testes. Seu software é literalmente crítico para a missão – uma oportunidade única. 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 de codificação e arquitetura de 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 vasto tópico 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 isto e pensarão que significa usar recursos de linguagem simples, 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 posteriores.
A Microsoft define complexidade ciclomática como:
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 complexidade ciclomática
baixa geralmente indica um método que é fácil de entender, testar e
manter.
O que é uma complexidade ciclomática baixa? A Microsoft recomenda manter 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 por 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 facilitam muito para o compilador JIT amortizar esse custo. Funções pequenas também têm mais probabilidade de serem candidatas para inlining. Não é apenas o tamanho: o fluxo de controle mais simples importa tanto quanto. Menos ramificações de controle dentro de funções facilitam para o compilador JIT registrar variáveis. Não é apenas uma 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 convoluta. Em 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 os incidentes de defeitos. Isto é tão importante e profundo que precisa ser repetido: Escrever código testável, sem escrever os testes reais, reduz os incidentes de defeitos.
Isto levanta a questão, o que é código testável?
Defino código testável como código que pode ser testado isoladamente. Isto significa que todas as dependências podem ser mockadas a partir de um teste. Um exemplo de dependência é uma consulta de 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 ao seguir os princípios de Inversion of Control (Dependency Injection) e S.O.L.I.D. Você ficará surpreso com a facilidade e se perguntará por que levou tanto tempo para começar a escrever desta forma.
3. Revisões de Código
Uma das práticas mais impactantes que uma equipe de desenvolvimento pode adotar é a revisão de código.
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 por cento na produtividade e uma diminuição de 90 por cento em defeitos após a organização introduzir revisões.
- A Aetna Insurance Company encontrou 82 por cento dos erros em um programa usando inspeções e foi capaz de diminuir seus recursos de desenvolvimento em 20 por cento.
- 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. Depois que todos os programas foram 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 por cento.
Se esses números não o convencerem 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 é a primeira coisa a desaparecer. 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 a versão de codificação 2 (versão 1 não tinha testes) com testes automatizados reduziu imediatamente os defeitos em 20%, mas com um custo de 30% adicional.
Outro estudo analisou Test Driven Development (TDD). Eles observaram um aumento na qualidade do código, mais de duas vezes, comparado a projetos similares que não usavam 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 a uma diminuição nos problemas relatados em campo quando ajustado para
o número de mudanças de pré-lançamento…
Um Exemplo
O código a seguir 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 de construtor) AgencySettingsRepository e EmailService como dependências. Isto nos permite mockálos a partir de um teste e testar isoladamente.
Abaixo está a versão refatorada.
Observe como os serviços são injetados no construtor. Isto nos permite controlar qual implementação é passada para o construtor SendMail. É então fácil passar dados fictícios e interceptar as chamadas do método 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 isoladamente. Estamos usando o framework de mocking 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 sem defeitos (se descobrir como, me avise!), mas seguindo as 4 práticas descritas neste artigo você verá uma diminuição nos defeitos encontrados em 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 ao seguir os princípios de Inversion of Control e 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 é um Engenheiro de IA com quase 30 anos de experiência em engenharia de software. Ele constrói sistemas de IA práticos—pipelines de conteúdo, agentes de infraestrutura e ferramentas que resolvem problemas reais—e compartilha o que está aprendendo ao longo do caminho. Conecte-se com ele nas redes sociais: X (@chuckconway) ou visite-o no YouTube e no SubStack.