4 Prácticas para Reducir tu Tasa de Defectos
17 de noviembre de 2015 • 10 min de lectura

Escribir software es una batalla entre la complejidad y la simplicidad. Lograr el equilibrio entre ambas es difícil. El compromiso está entre métodos largos no mantenibles y demasiada abstracción. Inclinarse demasiado en cualquier dirección perjudica la legibilidad del código y aumenta la probabilidad de defectos.
¿Son evitables los defectos? La NASA lo intenta, pero también hacen 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. Aunque 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 los defectos. Las primeras dos prácticas limitan la introducción de defectos y las últimas dos prácticas exponen los defectos. Cada práctica es un tema vasto por 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 esto significa usar características simples del lenguaje, pero este no es el caso: el 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 rutas 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:
Recuerda que traducir tu 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 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 mucho más fácil para el compilador JIT amortizar ese costo. Las funciones pequeñas también son más propensas a ser candidatas para inlining. No es solo la pequeñez: El flujo de control más simple importa igual de mucho. Menos ramas de control dentro de las funciones hacen más fácil para el compilador JIT registrar variables. No es solo una buena práctica escribir código más claro; es como creas 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 la complejidad y los defectos.
La línea anterior es un poco enrevesada. En los términos más simples: mantener el código simple reduce tu tasa de defectos.
2. Escribir Código Testeable
Los estudios han demostrado que escribir código testeable, sin escribir las pruebas reales, reduce los incidentes de defectos. Esto es tan importante y profundo que necesita repetirse: Escribir código testeable, sin escribir las pruebas reales, reduce los incidentes de defectos.
Esto plantea la pregunta, ¿qué es código testeable?
Defino código testeable 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 son simulados (falseados) y se hace una aserción del comportamiento esperado. Si la aserción es verdadera, la prueba pasa, si no, falla.
Escribir código testeable puede 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. Te sorprenderás de la facilidad y te preguntarás por qué tomó tanto tiempo empezar 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 son las revisiones de código.
Las Revisiones de Código facilitan el intercambio de conocimiento 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 caso 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 del 14 por ciento en productividad y una disminución del 90 por ciento en defectos 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 pudo 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 liberados 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 te convencen de adoptar revisiones de código, entonces estás destinado a derivar hacia un agujero negro mientras cantas Johnny Paycheck’s Take This Job and Shove It.
4. Pruebas Unitarias
Admito que cuando estoy contra una fecha límite, las pruebas son lo primero que se va. Pero los beneficios de las pruebas no pueden negarse como ilustran los siguientes estudios.
Microsoft realizó un estudio sobre la Efectividad de las Pruebas Unitarias. Encontraron que codificar la versión 2 (la versión 1 no tenía pruebas) con pruebas automatizadas inmediatamente redujo los defectos en un 20%, pero a un costo adicional del 30%.
Otro estudio examinó el Desarrollo Dirigido por Pruebas (TDD). Observaron un aumento en la calidad del código, más de dos veces, comparado con proyectos similares que no usaban TDD. Los proyectos TDD tomaron en promedio 15% más tiempo para desarrollar. Un efecto secundario del TDD fue que las pruebas sirvieron como documentación para las bibliotecas y API’s.
Por último, en un estudio sobre Cobertura de Pruebas y Defectos Post-Verificación:
… Encontramos que en ambos proyectos el aumento en la cobertura de pruebas está
asociado con la disminución en problemas reportados en campo cuando se ajusta por
el número de cambios pre-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 testeabilidad 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 testeable?
No, este código no es testeable porque no podemos simular AgencySettingsRepository
y EmailService
.
Ejemplo de Código Refactorizado
¿Cómo podemos hacer este código testeable?
Inyectamos (usando inyección por constructor) AgencySettingsRepository
y EmailService
como dependencias. Esto nos permite simularlos desde una prueba y probar en aislamiento.
A continuación está la versión refactorizada.
Nota cómo los servicios son inyectados 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 a métodos del 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 Pruebas
A continuación hay un ejemplo de pruebas en aislamiento. Estamos usando el framework 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 = "[email protected]" }
//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 malinterpretes, nunca escribirás código libre de defectos (¡si descubres cómo, házmelo saber!), pero siguiendo las 4 prácticas descritas en este artículo verás una disminución en los defectos encontrados en tu código.
Para recapitular, 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 Testeable 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 te ayudan a ti y al equipo a entender el dominio y el código que has escrito: solo tener que explicar tu código revelará problemas. Y por último, las Pruebas Unitarias pueden mejorar drásticamente la calidad de tu código y proporcionar documentación para futuros desarrolladores.
↑ Volver arribaTambién te puede gustar
- Modificar un Archivo Localmente Sin Actualizar el Repositorio Git Remoto 1 min de lectura
- Una Implementación de Búsqueda Binaria 1 min de lectura
- Los Beneficios de Usar un Framework de Construcción 2 min de lectura