2.1 Programar es una tarea sobrehumana

2.1 Programar es una tarea sobrehumana

Capítulo publicado el 14/3/2022 por Enric Caumons Gou (@caumons)
  • 10 min de lectura

La frase «Programar es una tarea sobrehumana» la dijo un profesor que tuve en la facultad y no la voy a olvidar nunca. Son unas pocas palabras que juntas tienen muchísimo significado y eran el preludio de lo que nos esperaba en el futuro. En el momento que las escuché realmente me impactaron, pero no pensé hasta qué punto serían ciertas.

En este capítulo voy a hablar de lo que significan para mí estas palabras y te explicaré algunos casos que las ilustran claramente.

¿Te imaginas escribir un libro perfecto?

Como ya he dicho, los ordenadores son tontos muy rápidos. Partiendo de esta premisa te puedes imaginar lo que puede pasar si no se lo dices todo perfectamente… ¡Puede ser el caos! Un ejemplo muy bueno es: escribir software es como escribir un libro perfecto, sin una sola falta de ortografía, sin una sola errata, donde todo sea semántica y sintácticamente perfecto, donde no haya ambigüedades que puedan llevar a múltiples interpretaciones y tampoco haya errores de concepto ni casos no contemplados. Si una o más de estas condiciones no se cumplen, entonces vamos a tener problemas, seguro.

Efectivamente, hay problemas más o menos fáciles de solucionar. Normalmente, los derivados de errores de sintaxis, erratas y faltas de ortografía suelen ser los más fáciles de encontrar y resolver. Sin embargo, a veces nos encontramos frente a cadenas de errores muy largas y crípticas en las que uno lleva a otro y así sucesivamente. En estos casos, tenemos que empezar por el primero de los problemas. A veces, arreglando solo uno solucionamos varios a la vez porque los otros eran consecuencia directa del primero y realmente no había nada mal allí.

Por otro lado, los problemas derivados de conflictos semánticos, ambigüedades, errores de concepto y casos no contemplados pueden ser un auténtico infierno y pueden llevar a la desesperación y frustración máximas. Estos son los casos en los que la estructura del código parece correcta, si lo lees parece ser coherente, pero lo que dice realmente no es correcto. Es decir, son los casos en los que el error o bug consiste en una «mentira bien escrita».

Podrás pensar: ¿por qué escriben cosas que no están bien, aunque estén bien escritas? La principal respuesta es que es muy difícil tener en cuenta la totalidad de casos que se puedan dar. «Programar es realmente una tarea sobrehumana porque es imposible contemplar todos los casos posibles». Es por ello que se hacen desarrollos incrementales y después de cada cambio se van haciendo posteriores actualizaciones de correcciones de errores, para ir solucionando los casos que no se habían previsto en un inicio, incoherencias que se detectan en el comportamiento de los sistemas, etc.

La gran pregunta es: ¿cómo solucionamos todos estos problemas? Para solucionarlos no hay una receta mágica, sino que se hace en base a la experiencia y pericia de cada desarrollador, o con la ayuda de varios, si se da el caso. Además, contamos con herramientas como ficheros de «logs», que suelen ser ficheros de texto donde se escribe de forma automática lo que va pasando (ojo porque desgraciadamente no siempre disponemos de ellos, o bien están mal hechos y la información que nos aportan no nos sirve de nada). También usamos mucho los llamados «debuggers», programas que nos permiten depurar software ejecutándolo paso a paso para poder inspeccionar valores de variables, hasta encontrar el punto en el que las cosas dejan de funcionar como deberían. Otra técnica muy extendida consiste en añadir «prints» (funciones que imprimen valores por consola) en el código, de forma temporal, para poder seguir la traza de ejecución de los programas y conocer el valor de las variables que nos interesen.

No obstante, hay ocasiones en las que el hecho de arreglar un error puede hacer aflorar otros bugs que estaban latentes, pero todavía no se habían manifestado porque el primer error evitaba que se ejecutara otro código igualmente erróneo, o bien que se diera la nueva casuística problemática.

Es muy importante usar herramientas de monitorización para tener feedback a tiempo real de cómo están nuestros sistemas y así podernos anticipar a problemas antes de que se produzcan. Por ejemplo, si actuamos cuando recibimos un aviso de falta de espacio en el disco duro del servidor, nos podemos ahorrar problemas de corrupciones de datos. Lo veremos con más detalle más adelante.

Un simple espacio es el fin del mundo

Este caso fue perverso. Se trataba de integrar una pasarela de pago en una plataforma online, haciendo una integración a través de unas API de un proveedor externo. Hasta aquí todo bien, lo malo es que una vez hecha la integración no funcionaba. Es decir, la plataforma que debía efectuar los cobros a los clientes que compraban los servicios online fallaba.

Revisé el código y todo parecía estar bien, la integración estaba hecha según la documentación del proveedor. Por lo tanto, contactamos con ellos para exponerles el problema y nos pusimos a investigar conjuntamente. El caso no era normal porque por su parte estaba todo configurado y su plataforma no tenía registrada ninguna incidencia.

Después de muchas horas, esfuerzo y frustración, les dije: «¿Estáis absolutamente seguros de que los tokens de autenticación son correctos? ¿No se os ha colado ningún espacio?». Efectivamente, ese era el problema, ¡había un maldito espacio al final de uno de los tokens y por eso no funcionaba! Cuando quitaron el espacio, todo funcionó perfectamente. Allí aprendí una valiosa lección que me ha servido en infinidad de ocasiones: siempre hay que limpiar los datos que vienen de fuentes externas, sobre todo, los introducidos por los usuarios.

Este es un caso perfecto para que puedas entender a lo que me refiero cuando hablamos de los errores semánticos. Para una persona es lo mismo escribir «hola» que escribir «hola » (fíjate en el espacio al final del segundo hola). Si lo lee una persona, puede entender perfectamente que se ha «colado» un espacio al final, pero sabe que las dos palabras son iguales. Una máquina no. La máquina en el primer caso ve una secuencia de 4 caracteres: h, o, l, a. En el segundo caso ve una secuencia de 5 caracteres: h, o, l, a, « ». Como las compara literalmente, entonces estas dos secuencias son distintas. Ves la diferencia respecto al razonamiento humano, ¿verdad? Pues así funcionan las cosas en la informática.

No te salgas del guion

A veces pienso que no me extraña que muchos del sector llevemos gafas, con lo mucho que llegamos a forzar la vista. Este ejemplo es uno de esos en los que «tienes que contar píxeles». Resulta que estaba escribiendo un manual para un procedimiento de restauración de backups con un procesador de textos. Hasta aquí todo bien, lo malo es que en ese manual incluí algún comando de consola, junto con texto e imágenes. Uno de los comandos contenía un guion «-» para especificar uno de los parámetros, con la mala pata que el programa lo transformó automáticamente en un guion largo «–» sin previo aviso, pero yo no me di cuenta y seguí escribiendo.

Una vez finalizado el manual, me puse a reproducir todos los pasos para verificar que el procedimiento era correcto, hasta que llegué a ese comando y no funcionaba. ¿Cómo podía ser que no funcionara? Los parámetros y la sintaxis eran correctos, pero no iba bien… Me pasé un buen rato tratando de averiguar qué diablos estaba pasando, incluso mirando los logs del servidor. No me lo podía creer, algo que era tan sencillo y que me estuviera dando estos problemas… Al final opté por escribir todo el comando letra a letra en vez de copiarlo del manual y entonces ¡sí funcionó correctamente! Mi cara era un poema, tenía el mismo comando que copiado no funcionaba, pero escrito manualmente, sí. Entonces pensé que esto debía de ser por algún espacio o carácter extraño no visible, así que lo comprobé y… nada, no había éxito. Copié en un editor de texto plano el comando problemático y en la línea de abajo el comando que había escrito que sí funcionaba bien. Empecé a revisar carácter a carácter para ver qué diferencia había, hasta que vi que en el comando copiado del manual había un guion largo «–», mientras que en el otro había un guion normal «-». Cambié el guion, volví a probarlo y entonces sí funcionó.

Sí, amigo mío, estas son algunas de las maravillas de la tecnología

Una puerta que se abre sola

Este caso fue una auténtica locura y ocurrió durante la fase de desarrollo de un sistema de apertura y cierre de puertas usando, entre otros componentes, Arduinos y relés activados con un mando a distancia. Resulta que, de forma totalmente aleatoria, la puerta se abría o cerraba sin pulsar el botón del mando. ¿Cómo podía ser eso?

Resumiéndolo muchísimo, te diré que se trataba de un problema con la inicialización por defecto de una variable. Era un problema entre un simple 1 y un 0 que causó un buen quebradero de cabeza. Si tienes curiosidad, puedes leer la descripción técnica a continuación para conocer en más profundidad los detalles de la hazaña.

Descripción técnica (opcional):

Usando el mando, accionaba a distancia un relé que abría o cerraba un sencillo circuito electrónico (pull down). Cuando se detectaba una variación en el voltaje del circuito mediante un Arduino, se interpretaba como una pulsación del mando. Entonces se enviaba la orden a otro relé que controlaba el motor de la puerta, haciendo que esta se abriera o cerrara, dependiendo de su estado.

¿Dónde podía estar el problema? ¿Cómo podía haber una variación en la tensión si no había problemas eléctricos en la casa y tampoco se había pulsado el botón del mando? Después de mucho sufrimiento, encontré el problema. Resultó que el Arduino se reiniciaba solo eventualmente y entonces se volvían a ejecutar sus dos funciones básicas. Es decir, la función de inicialización del microcontrolador, llamada setup(), y la función loop(), que se ejecuta indefinidamente hasta que se vuelva a reiniciar o lo apaguemos.

En la función setup() inicializaba por defecto el valor leído de la tensión del pull down en 0. Pero, dependiendo del estado en el que había quedado el relé, podía darse el caso de que al arrancar el Arduino el valor que leyera fuera 1 en vez del 0 por defecto. Cuando ocurría esto, al entrar en el loop() lo interpretaba como si se hubiera pulsado el botón del mando porque detectaba un cambio en el voltaje y se daba la orden errónea de abrir o cerrar la puerta.

Por supuesto, este problema también afectaba en el momento de encender el sistema y no solamente en los reinicios fortuitos, ya que había el 50 % de probabilidades de que la puerta se abriera o se cerrara sola en el momento del encendido, dependiendo de si el valor leído era un 1 en vez de un 0.

¿Cómo lo solucioné? Fácil, en vez de inicializar en 0 el valor leído del pull down, lo inicialicé leyendo su valor real en la función setup(). De esta forma me aseguré de que no se detectaría ninguna falsa variación al entrar en el loop(). Es decir, solo se detectarían cambios cuando se modificara el estado del relé o, lo que es lo mismo, cuando alguien pulsara el botón del mando a distancia.

Arquitecturas imposibles

Otra de las cosas por las que programar es una tarea sobrehumana consiste precisamente en la arquitectura de las aplicaciones. Los programas informáticos están formados por varios (muchos) ficheros de código fuente, organizados según sus funcionalidades (responsabilidades). Estos distintos componentes que forman los programas se relacionan entre ellos para llevar a cabo la acción o acciones para las cuales han sido desarrollados. Igual que en la vida real, una sola persona no puede hacerlo todo, por este motivo, las empresas se organizan en departamentos y equipos para que todo funcione, pues la idea es la misma.

Uno de los peores problemas que te puedes encontrar a nivel técnico es que la arquitectura de un sistema no se adapte a los nuevos requerimientos. Es decir, que la arquitectura actual no pueda soportar las nuevas funcionalidades debido a limitaciones existentes entre las dependencias y/o las responsabilidades de los distintos componentes que forman el programa. Además, otra situación crítica ocurre cuando se descubre que hay algún fallo de base, que hace que la arquitectura del proyecto no sea lo suficientemente sólida, y esto implica que habrá que hacer un gran esfuerzo para solucionar el problema.

A continuación, usaré el término «visibilidad» de forma genérica expresamente, ya que dependiendo del contexto en el que lo apliquemos tendrá un significado u otro. Por ejemplo, si hablamos de servidores podemos entender la visibilidad como que haya conexión o se pueda acceder, mientras que si hablamos de módulos (ficheros de código fuente) puede significar que uno pueda importar (incluir) a otro. Evidentemente, si este apartado es demasiado técnico para ti, te lo puedes saltar entero.

Imaginemos que tenemos tres componentes A, B y C, donde A tiene visibilidad sobre B (A → B) y B tiene visibilidad sobre C (B → C). Sin entrar en detalles, podemos decir que A tiene visibilidad sobre C de forma indirecta, pasando por B, gracias a la propiedad transitiva. El problema está en que los requerimientos siempre van cambiando y puede ser que eventualmente el componente C necesite acceder al componente A (C → A). En la arquitectura actual esto no sería posible y es entonces cuando nos empieza a venir el sudor frío pensando que «no se puede hacer».

Aquí es donde entran en acción el ingenio, la destreza y la experiencia. A priori, ante un caso así, podríamos pensar en tres opciones: modificar el componente C para que pueda tener visibilidad directa sobre A (C → A). También podríamos pensar en implementar un nuevo componente que llamaremos D, que proporcione visibilidad indirecta desde C sobre A, de forma que quedaría así: C → D → A. Por último, se podría modificar el componente C para que tuviera visibilidad sobre B (C → B) y hacer que B tenga visibilidad sobre A (B → A), de forma que tendríamos visibilidad indirecta de C sobre A, pasando por B: C → B → A. La gran pregunta es: ¿cuál de estas tres soluciones es mejor? La respuesta es: depende de cada caso. Pero, de entrada, la que me gusta menos es la tercera porque implica modificar todos los componentes existentes. Además, implicaría tener los componentes totalmente acoplados en los dos sentidos y esto nos puede llevar a problemas de dependencias circulares: A → B → C y C → B → A.

Imaginemos que el componente C lo hayamos hecho nosotros mismos y sea sencillo, entonces podría ser factible modificarlo para que pudiera acceder directamente sobre A. En cambio, si el componente C es un componente de terceros que no podemos modificar, entonces muy probablemente tengamos que implementar algún sistema (D) que recoja la salida de este componente C y la encamine hacia A.

A este ejemplo del nuevo componente «D» se le llama añadir un nivel más de indirección, añadir una capa más de software o incluso (dependiendo del contexto) añadir una capa de abstracción. Esta técnica es, sin duda, un as en la manga que nos va a salvar de más de un apuro. A veces nos centramos en querer modificar algo cuando, en realidad, añadir otro componente más nos facilita muchísimo la vida y va a hacer que el sistema sea mucho más mantenible en el futuro.

¡Únete a la comunidad para no perderte nada!

¡Quiero unirme!

¿Qué te ha parecido este capítulo?

¡Compártelo!

¡Suscríbete al feed para estar al día cada vez que se publique un nuevo capítulo!

Comprar libro

${ commentsData.total }

Todavía no hay comentarios. ¡Sé el primero!

Inicia sesión para publicar, responder o reaccionar a los comentarios.

Esta web utiliza cookies. Si continúas usándola, asumiremos que estás de acuerdo.