TL;DR: Falla rápido. No escondas los errores bajo la alfombra
Photo by chuttersnap on Unsplash
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) muchas llamadas más adelante.
Null: The Billion Dollar Mistake
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 performance 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.
<?php
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);
// will raise an exception.
// No, It will not coerce to March,1st
// or do “under the rug magic”
// to cover up the programmer contract violationDe 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).
<?
$coordinate = array(’latitude’=>1000, ‘longitude’=>2000);
// They are just arrays. A Bunch of raw dataEsto 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.
<?
final class GeographicCoordinate{
function __construct($latitude, $longitude) {
if (!$this->isValidLatitude($latitude)) {
throw new InvalidLatitudeException($latitude);
}
// ...
}
}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:
<?
final class Latitude {
function __construct($degrees) {
if (!$degrees->between(-90, 90)) {
throw new InvalidLatitudeException($degrees);
}
// ...
}
}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 más 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.
Con base en 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.
Foto por Robert Penaloza en Unsplash
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.
<?
$distance = 12.4; // miles
$supplyRatio = 10 ; // tons each kilometer
$neededSupply = $distance / $supplyRatio;
// since units could not be mixed the should raise an error
// but the units were all floats
// so the engine keep working and explodedLa 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.
<?
final class Movement {
function __construct($aParty, $aCounterParty, $anAmount, $aDate) {
if ($aDate < Date::today()) {
throw new InvalidMovementDateException($aDate);
} // ...
}
}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.
Acoplamiento: El único problema importante en el diseño de software
TL;DR: Por qué deberías eliminar el acoplamiento ya mismo
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.
Code Smells
Algunos code smells relacionados:
Code Smell 83 - Variables Reassignment
Code Smell 15 - Missed Preconditions
Code Smell 93 - Send me Anything
Code Smell 196 - Javascript Array Constructors
Code Smell 111 - Modifying Collections While Traversing
Code Smell 170 - Refactor with Functional Changes
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 acoplándonos 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 las observaciones y comentarios sobre esta nota.
Versión en inglés: Fail Fast







