Falla Rápido
Fallar y fracasar esta de moda. En un momento en que hacer es mucho más fácil que pensar y los fracasos no son estigmas llevemos esta idea a nuestro código.
En los años 50s fallar al programar tenía consecuencias terribles. El tiempo de máquina era costosísimo. El salto de las tarjetas perforadas al compilador y luego a la ejecución podía llevar horas o incluso días.
Por suerte esos tiempos ya quedaron atrás. ¿Quedaron atras?
Un retroceso metodológico
En la década del 80 ya no se utilizaban las tarjetas perforadas. El código se escribía en un editor de texto, luego el programa se compilaba y linkeaba para generar código ejecutable en una típica aplicación de escritorio. Este proceso era lento y tedioso.
Un error implicaba generar logs a archivo con partes del stack de ejecución para tratar de aislar la causa del defecto. Intentar una corrección, volver a compilar, linkear etc. y así de manera iterativa.
Con la llegada de los lenguajes interpretados empezamos a creer en la magia de editar el código ‘al vuelo’ con un debugger donde podíamos acceder al estado.
Sin embargo, a finales de los años noventa con el auge de los sistemas web retrocedimos varios pasos. Salvo en aquellos casos en que pudiéramos simular el sistema en un servidor local volvimos a poner logs en el código mientras depurábamos nuestro software integrado de manera remota.
Por otra parte. Gracias al mal uso de abstracciones inválidas nuestro software generaba errores muy lejos de la falla y causa raíz del problema.
Esto se agravó con uso de representaciones inválidas con posibles valores en Null que generan fallas no predichas al querer interpretar el origen de dicho valor (null) muchos llamadas más adelante.
Programación defensiva
El auge de los autos autónomos nos permite aprender sobre conductas de los choferes. Inicialmente, los autos funcionaban bien siguiendo las reglas de tráfico pero esto generaba accidentes con autos manejados por seres humanos. La solución consistió en entrenar a los autos autónomos para que manejen de manera defensiva.
Como en muchas de nuestras soluciones vamos a invertir la carga de la prueba.
Vamos a empezar a suponer que las precondiciones no se cumplen y en caso de ser así fallar rápido.
El argumento en contra de este tipo de controles es siempre el mismo. El código se hace ligeramente más complejo y, potencialmente, menos performante. Como siempre ante la vagancia contestaremos que privilegiamos el código robusto y ante la perfomance pediremos evidencia concreta mediante un benchmark que demuestre cuál es la penalidad verdadera.
Como vimos en el artículo acerca de la inmutabilidad de los objetos si se crea una fecha inválida debemos informar inmediatamente del problema.
final class Date{
function __construct($aMonthDay, $aMonth) {
if (!$aMonth->includes($aMonthDay)) {
throw new InvalidDateException($aMonthDay, $aMonth);
}
...
}$day30 = new Day(30);
$year2020 = new Year(2020);
$feb2020 = new YearMonth(2, $year2020);
$invalidDate = new Date($day30, $feb2020);
//lanzará una excepción
//No, No va a realizar una coercion oculta hacia el primero de marzo escondiendo la basura bajo la alfombra para cubrir la violación del contrato por el programador.
De este modo estaremos muy cerca del lugar donde se presenta la falla y podremos tomar acciones.
La mayoría de los lenguajes “modernos” esconden la suciedad bajo la alfombra y permiten “seguir como si nada” la ejecución para que nosotros tengamos que depurar con logs la causa del problema al correr para realizar un análisis forense en busca de la falla.
La representación siempre es importante
La mejor forma de fallar rápido es representar los objetos de manera adecuada respetando nuestra única regla de diseño: La biyección con el mundo real.
Una mala representación de una coordenada geográfica utilizando un array con dos enteros no va a saber ‘defenderse’ de posibles situaciones inválidas.
Por ejemplo podemos representar en un mapa la latitud 1000°, longitud 2000° de la siguiente manera y esto va a generar errores al querer calcular distancias en algún componente que utilice dicha coordenada (probablemente haciendo algún tipo de magia de módulo y consiguiendo pasajes muy baratos).
Esto se soluciona con buenas representaciones y con objetos pequeños que respeten la biyección tanto de comportamientos y estados válidos como inválidos.
La Biyección es clara: una coordenada no es un array. no todos los array son coordenadas.
Esta sería la primera iteración. La coordenada debería comprobar que la latitud este dentro de un rango. Pero eso sería acoplar la coordenada a la latitud y violar la regla de biyección. Una latitud no es un entero y viceversa.
Seamos extremistas:
Con esta solución no tenemos que hacer ningún chequeo al construir coordenadas geográficas porque la latitud es válida por invariante de construcción y porque está modelando correctamente a su par de la realidad.
Como última iteración deberíamos pensar qué es un degree. ¿un entero? ¿un float?. Está claro que un degree existe en la realidad así que tenemos que modelarlo.
A esta altura los puristas de la performance se suelen indignar con el siguiente pensamiento:
Es mucho mas fácil y legible crear una coordenada como un array que hacer toda esa indirección de crear degrees, latitudes, longitudes y coordenadas.
Para tomar esta decisión siempre tenemos que hacer análisis de performance, mantenibilidad, fiabilidad y causa raíz de nuestras fallas. En base a nuestros atributos de calidad deseados privilegiaremos uno sobre el otro. En mi experiencia personal los buenos y precisos modelos sobreviven mucho mejor al cambio y al efecto onda pero eso depende de cada caso particular.
Dejemos el tiempo y volvamos al espacio
Como último ejemplo volvamos a la situación que hizo estallar el cohete Mars Climater Orbiter mencionado en el artículo:
El cohete fue desarrollado por dos equipos de diferentes países que utilizaban distintos sistemas métricos. El ejemplo de abajo es una simplificación del escenario.
En vez de fallar temprano y ser atrapado por una rutina con código self healing este error se propagó e hizo estallar el cohete.
Un simple chequeo de medidas hubiera detectado el error y, potencialmente, tomado alguna acción correctiva.
La excepción es la regla
Nuestro código debe ser siempre defensivo y controlar por sus invariantes en todo momento como indica Bertrand Meyer. No alcanza con prender y apagar aserciones de software. Dichas aserciones deben estar siempre prendidas en ambientes productivos. Nuevamente ante la duda sobre penalizaciones de performance la respuesta contundente debe ser evidencia certera de una degradación importante.
Las excepciones deben ocurrir en todos lo niveles. Si un movimiento se crea con una fecha inválida debe informarse la excepción al crear la fecha. Si la fecha es válida pero es incompatible con alguna regla de negocio (por ejemplo no se pueden registrar movimientos en el pasado) esto también debe ser controlado.
La solución es robusta pero esta acoplando el movimiento a una fecha y a un método estático de una clase global. Uno de los peores acoplamientos posibles para un sistema que podría correr en múltiples zonas horarias.
Para solucionar este problema tenemos varias opciones:
- Dejar el acoplamiento a la clase.
- Enviar como parámetro un validador de fecha al que se le pueda pedir mediante double dispatch por la validez de la fecha.
- Sacar la responsabilidad de la validación de la fecha del movimiento.
Ante cualquier duda en nuestros diseños siempre podremos recurrir a la biyección y preguntarle a nuestro experto en el negocio a quien corresponde dicha responsabilidad.
Esta claro que, al tomar la tercera opción podríamos, potencialmente crear movimientos con fechas inválidas. Pero la validez o no de la fecha no es una responsabilidad del movimiento y no participa en sus invariantes de representación.
Distinto sería el caso de un movimiento con una fecha de acuerdo, una de creación y una de liquidación con restricciones de negocio claras entre ellas. Pero entonces estaríamos frente a un objeto poco cohesivo.
Como siempre las decisiones de diseño implican continuos trade offs.
Conclusiones
Ante la sospecha de una situación invalida debemos lanzar una excepción en todos los casos. Ante la duda hay que hacerlo lo más temprano posible.
Jamás debemos esconder errores acoplandonos la decisión de enmascarar este problema con el que tiene que interpretar esa situación.
Debemos seguir a rajatabla la regla de la biyección, creando las abstracciones necesarias que sepan defenderse.
Parte del objetivo de esta serie de artículos es generar debates y espacios de discusión sobre la problemática del diseño de software.
Esperamos ansiosamente los observaciones y comentarios sobre esta nota.
Este artículo fue publicado al mismo tiempo en inglés aquí.