Skip to content

Publicaciones

4 prácticas para reducir su tasa de defectos

17 de noviembre de 2015 • 10 min de lectura

4 prácticas para reducir su tasa de defectos

Escribir software es una batalla entre la complejidad y la simplicidad. Lograr un equilibrio entre los dos es difícil. El compromiso está entre métodos largos e inmantenibles y demasiada abstracción. Inclinarse demasiado en cualquier dirección afecta la legibilidad del código e incrementa la probabilidad de defectos.

¿Son evitables los defectos? La NASA lo intenta, pero también realizan cantidades enormes de pruebas. Su software es literalmente crítico para la misión – una oportunidad única. Para la mayoría de las organizaciones, este no es el caso y grandes cantidades de pruebas son costosas e impracticables. Si bien no hay sustituto para las pruebas, es posible escribir código resistente a defectos, sin pruebas.

En 20 años de codificación y arquitectura de aplicaciones, he identificado cuatro prácticas para reducir defectos. Las primeras dos prácticas limitan la introducción de defectos y las últimas dos prácticas exponen defectos. Cada práctica es un tema vasto en sí mismo sobre el cual se han escrito muchos libros. He destilado cada práctica en un par de párrafos y he proporcionado enlaces a información adicional cuando es posible.

1. Escribir código simple

Simple debería ser fácil, pero no lo es. Escribir código simple es difícil.

Algunos leerán esto y pensarán que significa usar características de lenguaje simples, pero este no es el caso — código simple no es código tonto.

Para mantenerlo objetivo, estoy usando la complejidad ciclomática como medida. Hay otras formas de medir la complejidad y otros tipos de complejidad, espero explorar estos temas en artículos posteriores.

Microsoft define la complejidad ciclomática como:

La complejidad ciclomática mide el número de caminos linealmente
independientes a través del método, que se determina por el número y
complejidad de las ramas condicionales. Una complejidad ciclomática
baja generalmente indica un método que es fácil de entender, probar y
mantener.

¿Qué es una complejidad ciclomática baja? Microsoft recomienda mantener la complejidad ciclomática por debajo de 25.

Para ser honesto, he encontrado que la recomendación de Microsoft de una complejidad ciclomática de 25 es demasiado alta. Para mantenibilidad y complejidad, he encontrado que el tamaño ideal del método está entre 1 a 10 líneas con una complejidad ciclomática entre 1 y 5.

Bill Wagner en Effective C#, Second Edition escribió sobre el tamaño del método:

Recuerde que traducir su código C# a código ejecutable por máquina es un proceso de dos pasos. El compilador de C# genera IL que se entrega en ensamblados. El compilador JIT genera código de máquina para cada método (o grupo de métodos, cuando está involucrado el inlining), según sea necesario. Las funciones pequeñas hacen que sea mucho más fácil para el compilador JIT amortizar ese costo. Las funciones pequeñas también tienen más probabilidades de ser candidatas para inlining. No se trata solo de pequeñez: el flujo de control más simple importa tanto. Menos ramas de control dentro de las funciones hacen que sea más fácil para el compilador JIT enregistrar variables. No es solo una buena práctica escribir código más claro; es cómo crea código más eficiente en tiempo de ejecución.

Para poner la complejidad ciclomática en perspectiva, el siguiente método tiene una complejidad 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;
}

Una hipótesis de complejidad generalmente aceptada postula que existe una correlación positiva entre complejidad y defectos.

La línea anterior es un poco enredada. En los términos más simples — mantener el código simple reduce su tasa de defectos.

2. Escribir código comprobable

Los estudios han demostrado que escribir código comprobable, sin escribir las pruebas reales, reduce los incidentes de defectos. Esto es tan importante y profundo que necesita repetirse: Escribir código comprobable, sin escribir las pruebas reales, reduce los incidentes de defectos.

Esto plantea la pregunta, ¿qué es código comprobable?

Defino el código comprobable como código que puede ser probado en aislamiento. Esto significa que todas las dependencias pueden ser simuladas desde una prueba. Un ejemplo de una dependencia es una consulta de base de datos. En una prueba, los datos se simulan (falsifican) y se realiza una afirmación del comportamiento esperado. Si la afirmación es verdadera, la prueba pasa, si no, falla.

Escribir código comprobable podría sonar difícil, pero, de hecho, es fácil cuando se siguen los principios de Inversión de Control (Inyección de Dependencias) y S.O.L.I.D. Se sorprenderá de la facilidad y se preguntará por qué tardó tanto en comenzar a escribir de esta manera.

3. Revisiones de código

Una de las prácticas más impactantes que un equipo de desarrollo puede adoptar es la revisión de código.

Las revisiones de código facilitan el intercambio de conocimientos entre desarrolladores. Hablando por experiencia, discutir abiertamente el código con otros desarrolladores ha tenido el mayor impacto en mis habilidades de escritura de código.

En el libro Code Complete, por Steve McConnell, Steve proporciona numerosos estudios de casos sobre los beneficios de las revisiones de código:

  • Un estudio de una organización en AT&T con más de 200 personas reportó un aumento de productividad del 14 por ciento y una disminución de defectos del 90 por ciento después de que la organización introdujo revisiones.
    • La Compañía de Seguros Aetna encontró el 82 por ciento de los errores en un programa usando inspecciones y fue capaz de disminuir sus recursos de desarrollo en un 20 por ciento.
    • En un grupo de 11 programas desarrollados por el mismo grupo de personas, los primeros 5 fueron desarrollados sin revisiones. Los 6 restantes fueron desarrollados con revisiones. Después de que todos los programas fueron lanzados a producción, los primeros 5 tuvieron un promedio de 4.5 errores por 100 líneas de código. Los 6 que habían sido inspeccionados tuvieron un promedio de solo 0.82 errores por 100. Las revisiones redujeron los errores en más del 80 por ciento.

Si esos números no lo convencen de adoptar revisiones de código, entonces está destinado a derivar hacia un agujero negro mientras canta Take This Job and Shove It de Johnny Paycheck.

4. Pruebas unitarias

Lo admito, cuando estoy contra un plazo, las pruebas son lo primero que se va. Pero los beneficios de las pruebas no pueden ser negados como ilustran los siguientes estudios.

Microsoft realizó un estudio sobre la Efectividad de las pruebas unitarias. Encontraron que la versión de codificación 2 (la versión 1 no tenía pruebas) con pruebas automatizadas redujo inmediatamente los defectos en un 20%, pero a un costo de un 30% adicional.

Otro estudio examinó el Desarrollo Dirigido por Pruebas (TDD). Observaron un aumento en la calidad del código, más de dos veces, en comparación con proyectos similares que no utilizaban TDD. Los proyectos TDD tardaron en promedio un 15% más en desarrollarse. Un efecto secundario de TDD fue que las pruebas sirvieron como documentación para las bibliotecas y API.

Por último, en un estudio sobre Cobertura de pruebas y defectos posteriores a la verificación:

… Encontramos que en ambos proyectos el aumento en la cobertura de
pruebas está asociado con una disminución en los problemas reportados
en el campo cuando se ajusta por el número de cambios previos al
lanzamiento…

Un ejemplo

El siguiente código tiene una complejidad 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;
                });
            }
        }
    }

Examinemos la comprobabilidad del código anterior.

¿Es este código simple?

Sí, lo es, la complejidad ciclomática está por debajo de 5.

¿Hay alguna dependencia?

Sí. Hay 2 servicios AgencySettingsRepository y EmailService.

¿Son los servicios simulables?

No, su creación está oculta dentro del método.

¿Es el código comprobable?

No, este código no es comprobable porque no podemos simular AgencySettingsRepository y EmailService.

Ejemplo de código refactorizado

¿Cómo podemos hacer que este código sea comprobable?

Inyectamos (usando inyección de constructor) AgencySettingsRepository y EmailService como dependencias. Esto nos permite simularlas desde una prueba y probar en aislamiento.

A continuación se muestra la versión refactorizada.

Observe cómo los servicios se inyectan en el constructor. Esto nos permite controlar qué implementación se pasa al constructor SendMail. Entonces es fácil pasar datos ficticios e interceptar las llamadas del método de servicio.

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;
                });
            }
        }
    }
}

Ejemplo de prueba

A continuación se muestra un ejemplo de prueba en aislamiento. Estamos usando el marco de simulación 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();

    }

Conclusión

Escribir código resistente a defectos es sorprendentemente fácil. No me entienda mal, nunca escribirá código libre de defectos (¡si descubre cómo, avíseme!), pero siguiendo las 4 prácticas descritas en este artículo verá una disminución en los defectos encontrados en su código.

Para resumir, Escribir código simple es mantener la complejidad ciclomática alrededor de 5 y el tamaño del método pequeño. Escribir código comprobable se logra fácilmente cuando se siguen los principios de Inversión de Control y S.O.L.I.D. Las revisiones de código lo ayudan a usted y al equipo a entender el dominio y el código que ha escrito — simplemente tener que explicar su código revelará problemas. Y por último, Las pruebas unitarias pueden mejorar drásticamente la calidad de su código y proporcionar documentación para desarrolladores futuros.

Autor: Chuck Conway es un Ingeniero de IA con casi 30 años de experiencia en ingeniería de software. Construye sistemas de IA prácticos—canalizaciones de contenido, agentes de infraestructura y herramientas que resuelven problemas reales—y comparte lo que está aprendiendo en el camino. Conéctate con él en redes sociales: X (@chuckconway) o visítalo en YouTube y en SubStack.

↑ Volver arriba

También te puede interesar