Cuando tu programa comience a ejecutarse en producción, las condiciones y casos que creías imposibles sucederán, punto. En realidad, es imposible asegurar que tu código tendrá condiciones de ejecución óptimas todo el tiempo. Como desarrollador, te gustaría asegurar que tus proyectos puedan hacer frente a las imperfecciones del mundo real. Por eso, técnicas como la programación defensiva siempre tendrán un lugar en tu caja de herramientas.
Este enfoque es el equivalente en programación a la conducción defensiva. En la conducción defensiva, asumes la responsabilidad de anticipar condiciones de riesgo y errores cometidos por otros conductores.
La programación defensiva, en comparación, requiere que reconozcas que le sucederán cosas malas a tu programa, y te insta a que diseñes en tomando esto en cuenta. Debes anticipar y prepararte para casos como corrupción de datos, tiempos de espera de conexión y funciones que reciben datos incorrectos. Al manejar deliberadamente casos que “nunca sucederán”, tu código estará preparado para funcionar mejor bajo esas circunstancias.
Basura entra, nada sale
Una de las técnicas de programación defensiva más simples es verificar las precondiciones y postcondiciones en tus funciones. Una precondición es algo que necesita ser verdadero antes de que tu código de función se ejecute. Una postcondición es algo que siempre debe ser verdadero después de que tu función termine su ejecución. Para ilustrar dónde ocurren estas verificaciones, observa el siguiente fragmento.
def function_name(arguments)
#verificar precondiciones
#function body
#verificar postcondiciones
#return
end
Generalmente, estas verificaciones son para suposiciones como:
- Los parámetros de entrada están entre un rango esperado
- Un puntero no es nulo
- La lógica de la función no alteró variables que no deberían cambiar
- La función liberó apropiadamente un manejador/handler de recurso externo (como un manejador de archivo)
- El resultado de un cálculo satisface alguna verificación numérica, como un CRC
- Contenedores como listas o pilas tienen el número requerido de elementos
Usar ambos tipos de verificación en la misma función no es obligatorio o incluso necesario. Por ejemplo, las siguientes funciones hacen uso de solo una verificación cada una.
# Verificación de precondición: asegurar que la entrada esté entre un rango dado, en este caso, mayor que 0
def square_root(radicand)
if(radicand < 0)
#Handle the error
end
#function body
end
# Verificación de postcondición: verificar que una variable no cambió
def no_side_effects(should_not_change)
should_not_change_initial_value = should_not_change
#function body
if(should_not_change != should_not_change_initial_value)
#Handle the error
end
#return
end
Estas verificaciones simples te dan mucho poder al establecer explícitamente qué hacer cuando algo malo sucede. Usarlas es una forma segura de fortalecer tus funciones y hacer tu código más seguro.
Para casos excepcionales, usa excepciones
El manejo de excepciones es una característica increíblemente poderosa que la mayoría de los lenguajes de programación modernos proveen. Debido a que es un tema muy amplio y las implementaciones difieren, no podré cubrirlo en un solo artículo. Aun así, si quieres aprender más sobre el tema, echa un vistazo a estos recursos:
Hay un par de ideas para sacar el máximo provecho de tus excepciones sin importar qué lenguaje uses:
- Recuerda la característica más importante de una excepción: una vez que la disparas, no puedes ignorarla. Te obliga a manejar deliberadamente el error.
- Al lanzar la excepción, incluye toda la información necesaria para entender qué salió mal. Evita escribir código como el siguiente fragmento:
// Los bloques catch (o su equivalente) deben al menos registrar el mensaje de excepción.
public void badUseOfExceptions() {
try {
// Realizar algunas operaciones que podrían lanzar excepciones.
}
catch (Exception e) {
// No dejes esto vacío, al menos registra un mensaje para entender por qué el programa lanzó la excepción
}
}
- Aunque las excepciones son una herramienta extremadamente poderosa, necesitas usarlas con precaución. El uso excesivo de excepciones puede hacer que tu programa sea más difícil de seguir y alterar el flujo de ejecución.
- Las excepciones debilitan la encapsulación al requerir que el código que llama a una función sepa qué excepciones podría lanzar. Si puedes manejar el error localmente, hazlo.
- Las excepciones (como su nombre implica) están destinadas a ser usadas para manejar circunstancias excepcionales. Cuando tengas dudas, pregúntate: ¿si elimino todos los manejadores de excepción, funcionará mi programa? Si tu respuesta es no, podrías estar usándolas en casos que no son realmente excepcionales.
Maneja el error, no lo ignores
Entonces, tu código hace buen uso de las excepciones y todas tus funciones son sólidas como una roca. Dependiendo del tipo de error o datos que estés manipulando, puedes decidir hacer cosas como:
- Devolver un valor neutro o inofensivo, como un 0 de un cálculo numérico.
- En procesamiento de flujos (stream processing), devolver la última instancia de datos válida.
- Devolver el valor válido más cercano. Como devolver 0 de una aplicación de termómetro (en Kelvin) que encuentra un valor negativo para la temperatura. Registrar un mensaje de advertencia o error.
- Devolver un código de error para que el código que llamó tu función lo maneje o dispare una excepción.
- Apagar el sistema.
El enfoque que elijas para manejar el error depende de tus necesidades de robustez y corrección.
¿Tu código es robusto o es correcto?
Los requerimientos de robustez y la corrección son cualidades que influirán significativamente en la forma en que manejas los errores en tu código.
Se dice que un sistema de software es correcto si nunca devuelve un resultado inexacto. Un sistema robusto, por otro lado, continúa operando incluso si conduce a resultados inexactos.
La estrategia que elijas para el manejo de errores tenderá a favorecer una propiedad en detrimento de la otra. Por ejemplo, devolver una cadena vacía en caso de error podría ser incorrecto, pero el programa seguirá funcionando, lo que implica una mejora en la robustez del código en detrimento de su corrección. En contraste, apagar el programa evitará devolver datos malos, haciendo el programa más correcto y menos robusto.
La necesidad de robustez y corrección varía según la aplicación. Está bien cuando un videojuego sigue funcionando a pesar de colorear un píxel del tono incorrecto de verde. Por el contrario, no quieres que las máquinas de radiación operen incluso si el error es pequeño. En general, el software crítico para la seguridad favorece la corrección, mientras que las aplicaciones de consumo favorecen la robustez.
Un poco de atención al detalle marca toda la diferencia
La programación defensiva es un tema amplio, pero no necesitas abordar todo el corpus de conocimiento de una sola vez. El proceso de aprendizaje es incremental; cada nueva técnica que aprendas es una adición valiosa a tu caja de herramientas.
Siempre que sea posible, presta atención a cómo tu programa maneja los errores, sé intencional y no dejes nada al azar. La inversión es pequeña comparada con los retornos que obtendrás una vez que tu programa comience a ejecutarse en producción.
Qué hacer a continuación:
- Comparte este artículo con amigos y colegas. Gracias por ayudarme a llegar a personas que podrían encontrar útil esta información.
- Carl Vitullo escribió un excelente artículo sobre los problemas de hacer demasiada programación defensiva, puedes encontrarlo aquí.
- Puedes encontrar más información sobre manejo de errores en el capítulo 7 de Clean Code, y sobre programación defensiva en el capítulo 8 de Code Complete.