¡Con (fuera) la aplicación falla, por favor!
Publicado: 2020-02-12Los programadores intentan evitar fallas en su código. Si alguien usa su aplicación, no debería romperse o cerrarse inesperadamente. Esta es una de las medidas de calidad más sencillas: si una aplicación falla con frecuencia, es probable que no se haya hecho bien.
Los bloqueos de aplicaciones ocurren cuando un programa está a punto de hacer algo indefinido o malo , como dividir un valor por cero o acceder a recursos restringidos en una máquina. También podría hacerlo explícitamente el programador que escribió la aplicación. “Eso nunca sucederá, así que me lo saltaré”, es un pensamiento bastante común y no del todo irrazonable. Hay algunos casos que simplemente no pueden ocurrir, nunca, hasta que… ocurre.
Promesas rotas
Uno de los casos más comunes en los que sabemos que algo no puede pasar es el de las API. Hemos acordado entre backend y frontend: esa es la única respuesta del servidor que puede obtener para esta solicitud. Los mantenedores de esta biblioteca han documentado el comportamiento de esta función. La función no puede hacer nada más. Ambas formas de pensar son correctas, pero ambas pueden causar problemas.
Cuando está utilizando una biblioteca, puede confiar en las herramientas de lenguaje para ayudarlo a manejar todos los casos posibles. Si el lenguaje que usa carece de cualquier forma de verificación de tipo o análisis estático, debe encargarse de eso usted mismo. Aún así, puede verificar eso antes de enviarlo al entorno de producción para que no sea un gran problema. Eso puede ser difícil, pero lee los registros de cambios antes de actualizar sus dependencias y escribir pruebas unitarias, ¿verdad? Ya sea que use o haga una biblioteca, la escritura más estricta que pueda proporcionar es mejor para su código y otros programadores.
La comunicación backend-frontend es un poco más difícil. A menudo está débilmente acoplado, por lo que el cambio en un lado se puede hacer fácilmente sin saber cómo afectará al otro lado. Los cambios en el backend a menudo pueden romper sus suposiciones en el frontend y ambos a menudo se distribuyen por separado. Tiene que acabar mal. Solo somos humanos y a veces sucede que no entendimos al otro lado o nos olvidamos de contarles sobre ese pequeño cambio. Nuevamente, eso no es gran cosa con el manejo adecuado de la red: la respuesta de decodificación fallará y sabemos cómo manejarla. Sin embargo, incluso el mejor código de decodificación puede verse afectado por un mal diseño...
Funciones parciales. Mal diseño.
"Tendremos dos variables booleanas aquí: 'isActive' y 'canTransfer', por supuesto que no puedes transferir cuando no está activo, pero eso es solo un detalle". Aquí comienza, nuestro mal diseño que puede golpear duro. Ahora alguien creará una función con esos dos argumentos y procesará algunos datos basados en ellos. La solución más simple es... simplemente colapsar en un estado inválido, nunca debería suceder, por lo que no debería importarnos. Incluso a veces nos preocupamos y dejamos algún comentario para arreglarlo más tarde o para preguntar qué debería pasar, pero eventualmente se puede enviar sin completar esa tarea.
// pseudocódigo function doTransfer(Bool isActive, Bool canTransfer) { Si (está activo y puede transferir) { // hacer algo para la transferencia disponible } else if (no esActivo y no puedeTransferir) { // hacer algo para la transferencia no disponible } else if (está activo y no puede transferirse) { // hacer algo para la transferencia no disponible } else { // aka ( no isActive y canTransfer ) // hay cuatro estados posibles // esto no debería suceder, la transferencia no debería estar disponible cuando no está activa choque() } }
Este ejemplo puede parecer tonto, pero a veces puede caer en ese tipo de trampa que es un poco más difícil de detectar y resolver que esta. Terminarás con algo llamado función parcial. Esta es una función que se define solo para algunas de sus posibles entradas ignorando o chocando con otras. Siempre debe evitar las funciones parciales (tenga en cuenta que en los lenguajes de escritura dinámica, la mayoría de las funciones pueden tratarse como parciales). Si su idioma no puede garantizar un comportamiento adecuado con la verificación de tipos y el análisis estático, es posible que se bloquee después de un tiempo de forma inesperada. El código está en constante evolución y las suposiciones de ayer podrían no ser válidas hoy.
Fallar rapido. Fallar a menudo.
¿Cómo puedes protegerte? ¡La mejor defensa es el ataque! Hay un dicho agradable: “Fracasa rápido. Fallar a menudo”. Pero, ¿no acordamos simplemente que deberíamos evitar bloqueos de aplicaciones, funciones parciales y mal diseño? Erlang OTP brinda a los programadores una ventaja mítica de que se curará solo después de estados inesperados y se actualizará mientras se ejecuta. Pueden permitírselo, pero no todo el mundo tiene este tipo de lujo. Entonces, ¿por qué deberíamos fallar rápido y con frecuencia?
En primer lugar, encontrar esos estados y comportamientos inesperados . Si no verifica si el estado de su aplicación es correcto, ¡podría tener resultados aún peores que fallar!
En segundo lugar, para ayudar a otros programadores a colaborar en la misma base de código . Si estás solo en un proyecto en este momento, es posible que haya alguien más detrás de ti. Es posible que olvide algunas suposiciones y requisitos. Es bastante común no leer la documentación proporcionada hasta que todo funcione o no documentar los métodos y tipos internos en absoluto. En ese estado, alguien llama a una de las funciones disponibles con un valor inesperado pero válido. Por ejemplo, digamos que tenemos una función de 'esperar' que toma cualquier valor entero y espera esa cantidad de segundos. ¿Qué pasa si alguien le pasa '-17'? Si no falla inmediatamente después de hacerlo, podría generar algunos errores graves y estados no válidos. ¿Espera para siempre o no espera en absoluto?

La parte más importante del bloqueo intencional es hacerlo con gracia . Si bloquea su aplicación, debe proporcionar cierta información para permitir un diagnóstico. Es bastante fácil cuando usa un depurador, pero debería tener alguna forma de informar los bloqueos de la aplicación sin él. Puede usar sistemas de registro para conservar esa información entre los inicios de la aplicación o mirarla externamente.
La segunda parte más importante del bloqueo intencional es evitar eso en el entorno de producción...
No falles. Alguna vez.
Eventualmente enviarás tu código. No se puede hacer perfecto, a menudo es demasiado caro pensar siquiera en hacer garantías de corrección. Sin embargo, debe asegurarse de que no se comporte mal o se bloquee. ¿Cómo puedes lograr eso si ya decidimos estrellarnos rápido y con frecuencia?
Una parte importante del bloqueo intencional es hacerlo solo en entornos que no son de producción . Debe usar aserciones que se eliminen en las compilaciones de producción de su aplicación. Esto ayudará durante el desarrollo y permitirá detectar problemas sin afectar a los usuarios finales. Sin embargo, es mejor fallar algunas veces para evitar estados de aplicación inválidos. ¿Cómo podemos lograr eso si ya hemos hecho funciones parciales?
Haga que los estados indefinidos e inválidos sean imposibles de representar y, de lo contrario, recurra a los válidos. Eso puede parecer fácil, pero requiere mucho pensamiento y trabajo. No importa cuánto sea, siempre es menos que buscar errores, hacer arreglos temporales y… molestar a los usuarios. Automáticamente hará que algunas de las funciones parciales sean menos probables de suceder.
// pseudocódigo function doTransfer(Estado estado) { cambiar ( estado ) { caso Estado.canTransfer { // hacer algo para la transferencia disponible } caso Estado.cannotTransfer { // hacer algo para la transferencia no disponible } caso Estado.notActive { // hacer algo para la transferencia no disponible } // Es imposible representar la transferencia disponible sin estar activa // solo hay tres estados posibles } }
¿Cómo se pueden hacer imposibles los estados inválidos? Escojamos dos de los ejemplos anteriores. En el caso de nuestras dos variables booleanas 'isActive' y 'canTransfer', podemos cambiar esas dos en una sola enumeración. Representará exhaustivamente todos los estados posibles y válidos. Incluso entonces, alguien puede enviar variables indefinidas, pero eso es mucho más fácil de manejar. Será un valor no válido que no se importará a nuestro programa en lugar de pasar un estado no válido dentro, lo que hará que todo sea más difícil.
Nuestra función de espera también se puede mejorar muy bien en lenguajes fuertemente tipados. Podemos hacer que use solo enteros sin signo en la entrada. Eso por sí solo solucionará todos nuestros problemas, ya que el compilador eliminará los argumentos no válidos. Pero, ¿y si tu idioma no tiene tipos? Tenemos algunas soluciones posibles. Primero: simplemente bloquee, esta función no está definida para números negativos y no haremos cosas inválidas o indefinidas. Tendremos que encontrar un uso no válido durante las pruebas. Las pruebas unitarias (que deberíamos hacer de todos modos) serán muy importantes aquí. Segundo: esto puede ser arriesgado, pero dependiendo del contexto puede ser útil. Podemos recurrir a valores válidos manteniendo la aserción en compilaciones que no son de producción para corregir estados no válidos cuando sea posible. Puede que no sea una buena solución para funciones como esta, pero si hacemos un valor absoluto de un número entero, evitaremos que la aplicación se cuelgue. Dependiendo del idioma concreto, también podría ser una buena idea lanzar/generar algún error/excepción en su lugar. Podría valer la pena retroceder si es posible, incluso cuando el usuario ve un error, es una experiencia mucho mejor que fallar.
Tomemos un ejemplo más aquí. Si el estado de los datos de usuario en su aplicación frontend está a punto de ser inválido en algunos casos, podría ser mejor forzar un cierre de sesión y obtener datos válidos nuevamente del servidor en lugar de fallar. El usuario puede verse obligado a hacerlo de todos modos o puede quedar atrapado dentro de un bucle de bloqueo sin fin. Una vez más, debemos afirmar y fallar en tales situaciones en entornos que no sean de producción, pero no permita que sus usuarios sean evaluadores externos.
Resumen
A nadie le gustan las aplicaciones que fallan o son inestables. No nos gusta fabricarlos o usarlos. Fallar rápidamente con afirmaciones que brindan un diagnóstico útil durante el desarrollo y las pruebas detectará muchos problemas temprano. La alternativa a los estados válidos en producción hará que su aplicación sea mucho más estable. Hacer que los estados inválidos sean irrepresentables eliminará toda una clase de problemas. Dese un poco más de tiempo para pensar antes del desarrollo sobre cómo eliminar y recurrir a estados no válidos y un poco más durante la escritura para incluir algunas afirmaciones. ¡Puede comenzar a mejorar sus aplicaciones hoy mismo!
Lee mas:
- Diseño por contrato
- tipo de datos algebraicos