Skip to content

Posts

4 Práticas para Reduzir Sua Taxa de Defeitos

17 de novembro de 2015 • 9 min de leitura

4 Práticas para Reduzir Sua Taxa de Defeitos

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.

↑ Voltar ao topo

Você também pode gostar