- Algunos proyectos de software son de tipo sangre caliente y solo se mantienen si hay actividad de desarrollo continua; si la actividad se detiene pero luego puede retomarse, se acercan más al tipo sangre fría
- Los proyectos de sangre fría eligen tecnología aburrida para que, aunque pasen mucho tiempo detenidos, ni el build ni las pruebas se rompan, y evitan depender de servicios externos que puedan cambiar o desaparecer
- La adquisición o cierre de servicios de los que dependen, las actualizaciones del compilador y el fin del soporte de paquetes regresan como costo de mantenimiento cuando se intenta reactivar un proyecto con poca actividad
- En código que no se toca durante 1, 2 o 3 años, como en proyectos personales, es difícil generar calor de forma continua, por lo que debe diseñarse desde el inicio asumiendo una baja tasa de cambio
- El generador de sitios estáticos para este blog, desde su primer commit en 2012, ha seguido funcionando casi sin cambios solo con Python 2, 4 módulos de terceros incluidos en el repositorio, ejecución local y despliegue con
rsyncoverssh
Cómo mantener un proyecto usando la analogía de los animales de sangre fría
- En una clase de historia natural de 2004, el profesor dio la lección con una cría de painted turtle traída del congelador y puesta bajo una cámara
- La cría de painted turtle era una de las pocas especies capaces de sobrevivir incluso estando congelada
- Durante una hora, la tortuga pasó de movimientos casi imperceptibles a desplazarse, al final, aproximadamente la mitad de la pantalla
- Los animales de sangre caliente deben mantener su temperatura corporal dentro de un rango estrecho, y en los humanos aparecen problemas si se sale de alrededor de 37°C
- Los animales de sangre fría ajustan su metabolismo a la temperatura del entorno; cuando hace calor están activos, y a medida que el cuerpo y el ambiente se enfrían, se mueven más lentamente
- Los proyectos de software también pueden dividirse de forma similar
- El software de sangre caliente funciona bien cuando hay movimiento y calor constantes en el proyecto
- Si se deja detenido durante 6 meses, puede parecer un proyecto muerto cuando se vuelve a sacar
Condiciones y ejemplos de software de sangre fría
- La razón por la que a un proyecto de sangre caliente le cuesta reiniciarse es que los cambios externos se van acumulando
- El servicio del que depende el CI puede haber sido adquirido o haberse quedado sin dinero y dejar de funcionar
- Al intentar agregar una nueva dependencia, puede volverse necesario un upgrade del compilador
- Otros paquetes pueden haber quedado sin soporte y ya no funcionar con el compilador más reciente
- Si trabajas solo y solo cambias algo cuando te llega la inspiración, dejando el proyecto sin tocar durante más de un año, es difícil operarlo como uno de sangre caliente
- Un proyecto de sangre fría, como la cría congelada de painted turtle, debe poder retomarse dentro de un año exactamente desde donde se detuvo
- Para eso se usa boring technology y se evita depender de servicios externos donde los scripts de build y pruebas puedan cambiar, romperse o desaparecer por completo
- Las dependencias se incluyen dentro del repositorio del proyecto, como en el enfoque de vendored dependencies
- El software que impulsa este blog es un ejemplo de proyecto de sangre fría
- El primer commit fue el 8 de enero de 2012, y era un pequeño generador de sitios estáticos para reemplazar una instalación antigua de Wordpress
- Está escrito en Python 2, depende de 4 módulos de terceros y todos están committeados en el repositorio del proyecto
- Todo el proceso se ejecuta localmente, y el resultado se despliega con
rsyncoverssh - Salvo algunas mejoras pequeñas, ha seguido funcionando sin cambios y se espera que siga funcionando dentro de otros 12 años
1 comentarios
Opiniones en Hacker News
Express, el framework web del ecosistema de Node y JavaScript, mantiene actualmente su versión principal 4.x.x desde hace más de 10 años https://www.npmjs.com/package/express?activeTab=versions
Aun así, se usa mucho, al punto de tener más de 17 millones de descargas por semana https://www.npmjs.com/package/express, y aunque no le sobren funciones ni tenga el mejor rendimiento https://fastify.dev/benchmarks/, lo bueno es que permite desarrollar de forma rápida y estable y planificar a largo plazo
No hay que preocuparse por el fin de los parches de seguridad de versiones antiguas ni por cambios drásticos de API; y Go es aún más estable gracias a su amplia biblioteca estándar y su promesa de compatibilidad, que permite ejecutar programas de más de 10 años https://go.dev/doc/go1compat
Cuando apareció, mucha gente lo trataba como basura para falsos programadores mediocres solo por estar escrito en JavaScript, pero desde entonces ayudó a varias empresas a crear servicios geniales que generan dinero real, y hoy seguramente está manejando una enorme cantidad de requests
Hoy en día también escribo mucho en Go, pero sigo estando perfectamente conforme con crear servicios con Express y, en general, me parece un buen software
No es que realmente lo odie, más bien creo que no lo elegiría por la rueda interminable de actualizaciones de versión
Python es un muy mal ejemplo de software de sangre fría
Hay cambios incompatibles constantes tanto en el runtime como en las herramientas, y el autor también está en una situación en la que debe seguir usando Python 2, cuyo soporte terminó hace mucho
Mejores ejemplos serían lenguajes como Go o Java, donde código de hace 10 años sigue funcionando bien con herramientas modernas; o, más extremo aún, Perl, donde código de hace 30 años todavía funciona bien
Al crear software, uno comete errores que permiten que los usuarios hagan cosas de formas no previstas, y en el mundo Java eso se resuelve agregando funciones más nuevas, seguras y con intención más clara, y recomendando a los usuarios migrar
Python hace algo parecido, pero le agrega “y pronto desactivaremos la función antigua”, mientras que Java no hace eso
Por ejemplo, el método
equalsdejava.net.URLes conocido por tener un diseño roto y se desaconseja fuertemente, pero se sigue soportando desde hace más de 20 añosEn Python Airflow, el operador vacío admitió durante un tiempo el nombre
DummyOperator, pero como “dummy” se usó históricamente y culturalmente como término peyorativo, los mantenedores cambiaron para que se useEmptyOperatory rompieron el nombre anteriorAl actualizar, el código fallaba al cargarse hasta que se cambiara el nombre de la referencia, y personalmente no creo que rompería a los usuarios de esa manera
En el mundo Java, un cambio de nombre así se puede resolver con una sustitución de texto, así que probablemente lo habrían seguido soportando hasta que hubiera una razón para no poder hacerlo
Por eso, en general, creo que Java y las dependencias de bibliotecas Java se pueden actualizar con mucha más libertad que Python
Si usas una versión Java LTS y eliges buenas dependencias, puedes hacer que vuelva a ejecutarse cuando sea
En Python, durante una clase de machine learning, una dependencia introdujo de la noche a la mañana un cambio de API incompatible, y el instructor no se dio cuenta porque seguía usando la versión más reciente de unas semanas antes, cuando empezó a preparar la clase
El paso de Python 2 a 3 fue un cambio incompatible, pero fue un cambio único, no “cambios incompatibles constantes”
Si te mantienes dentro de la misma versión principal, las nuevas versiones menores no rompen código antiguo; por ejemplo, el código 2.x antiguo funciona bien en 2.7 y el código 3.x antiguo funciona bien incluso en 3.12
Una versión menor puede agregar nuevas funciones, pero el código 3.x antiguo no se rompe por no usar la palabra clave
asyncni type hintsEsta es una de las razones por las que evito Python cuando puedo
Siento que es poco probable que el código Python que escribo hoy siga funcionando dentro de unos años, y me parece un problema bastante grande
Incluso al intentar ejecutar código Java de hace 3 años con un SDK nuevo, siempre había algo roto
Trabajo en mainframes de IBM (z/OS) y, en cuanto a mantener la compatibilidad hacia atrás, casi no he visto a nadie acercarse tanto como IBM.
Creo que Microsoft Windows está en segundo lugar y la ABI del kernel de Linux quizá en tercero, pero si miramos todo el ecosistema Linux, eso no es más que una pequeña parte.
La mayoría de lo demás se acerca más al churn, y en el open source parece que poca gente quiere dedicar su tiempo libre a la compatibilidad hacia atrás.
Económicamente se parece al dilema del prisionero: todos le pasan a otros el costo de mantener la compatibilidad y, como resultado, terminan creando más trabajo inútil para todos.
Por ejemplo, si miras la comunidad de retrocomputación, no es raro que escriban drivers para que hardware nuevo funcione en sistemas operativos antiguos.
Si no hay remuneración, al final depende de cuánto aprecies la plataforma que estás creando, y yo decidí apuntar directamente al kernel de Linux mediante llamadas al sistema por su compromiso comprobado con la estabilidad de la ABI.
En cambio, con un lenguaje de programación que hice yo mismo, quiero que sea lo más “perfecto” posible, así que me dan ganas de seguir corrigiéndolo.
Por si alguien quisiera usarlo de forma descontrolada, dejé en el README un aviso de que todavía está en etapa temprana de desarrollo y es inestable.
Imagino que quienes crean Ruby o Python se sienten de forma parecida; como el lenguaje se siente como un hijo y quieren que tenga éxito, pueden pensar que hay que corregir errores como que
printhaya sido una palabra clave.De hecho, muchas veces se rompe precisamente la parte destinada a la compatibilidad hacia atrás.
En mi trabajo anterior hacíamos apps Node en contenedores, y el CI construía imágenes a partir del código fuente de Node; de pronto empezaron a fallar los despliegues de servicios que no se habían tocado en un tiempo.
Resultó que el Dockerfile estaba basado en una imagen de Ubuntu cuyo período de soporte había terminado, y los repositorios de actualizaciones se habían movido a repositorios de archivo, así que no se podía construir la imagen sin corregir el Dockerfile.
Es un ejemplo de software que se rompe aunque no lo toques, y por eso prefiero Go y los binarios únicos.
Si lo empaquetas como release, ni siquiera necesitas volver a compilarlo, y en una imagen Docker Distroless no hay dependencias aparte de mi binario.
He usado Go durante mucho tiempo y nunca sufrí el problema de que el software se deteriorara con la edad; desaparecieron muchos tipos de problemas que sentía al usar Node o PHP.
En Node, el segundo mayor problema son los patrones de indirección de los frameworks, y el primero es la gestión de paquetes.
Siguen apareciendo problemas de peer dependencies como “instalé la versión X, pero el módulo Y necesita la versión Z”.
Muchos ingenieros, cuando buscan una biblioteca en GitHub, revisan la hora del último commit.
Tienden a pensar que cuanto más reciente sea el commit, mejor soporte tiene la biblioteca.
Pero si un proyecto archivado hace exactamente lo que necesitas, tiene 0 bugs y ha sido estable durante años, es como encontrar una joya escondida en una tienda de segunda mano.
Hoy muchos ingenieros descartan automáticamente las bibliotecas que no se actualizan “constantemente”, y parecen considerarlo algo bueno.
Los entornos modernos de desarrollo de software muchas veces no son así, y el frontend web es un ejemplo típico de algo que cambia con frecuencia.
Si es una biblioteca totalmente independiente, quizá esté bien que no tenga actualizaciones, pero una biblioteca que depende de un framework de frontend web causará problemas si no se actualiza de acuerdo con los cambios del ecosistema.
No conozco las cifras reales, pero creo que en la abrumadora mayoría de los casos la falta de actividad reciente significa “abandonado”, no “terminado y sin bugs”.
Algunos lenguajes terminaron siendo casi distintos de su versión 1.0, mientras que otros conservaron la mayor parte del código escrito y solo añadieron cosas encima.
Al final, esa tendencia parece reflejarse también en la comunidad y el ecosistema.
Recuerdo que Clojure estaba entre los primeros de la lista porque casi no hace cambios que rompan compatibilidad, y una biblioteca que cambió por última vez hace 5 años sigue funcionando perfectamente con la versión actual del lenguaje.
Que sea de la familia Lisp, y por tanto permita extender el núcleo del lenguaje sin cambios upstream, también parece ayudar, aunque por supuesto tiene sus propios defectos.
Aun así, me gustó que me hiciera dejar de pensar que “frescura” equivale a “excelencia”.
Hoy uso más a menudo bibliotecas que casi no han cambiado desde hace años que bibliotecas creadas el año pasado, y no he tenido grandes problemas.
Algunos lenguajes tienen releases cada 1 o 2 años y agregan sintaxis nueva y elegante, o tipos de datos abstractos en la biblioteca estándar, para reemplazar patrones muy usados pero incómodos.
La comunidad de ese lenguaje casi de inmediato considera “idiomática” la nueva sintaxis, y cree que el código escrito con las viejas formas torpes debe corregirse.
La razón para cambiar una base de código concreta suele ser que, comparado con la sintaxis nueva, el enfoque existente es más opaco y dificulta el mantenimiento y la revisión de código.
La lógica es que, si la nueva sintaxis hubiera existido desde el principio, nadie habría considerado buen código la forma antigua, así que hay que actualizar el código para mejorar la legibilidad para los nuevos desarrolladores y bajar la barrera de entrada para contribuir.
Si una biblioteca implementada en un lenguaje de este tipo no se actualizó en más de 3 años, a menudo es una mala señal.
Puede significar que el desarrollador no está lo bastante conectado con la comunidad como para mantenerla con código idiomático que otros desarrolladores, formados en la versión más reciente del lenguaje, puedan leer fácilmente, y quizá tampoco tenga interés en aceptar PRs externos.
Puede que haya vulnerabilidades de seguridad, solo que nadie las reporta porque el proyecto aparece como abandonado.
El software que puede vivir sin actualizaciones es solo el software que se hizo bien desde el principio.
Si es software solo para uno mismo, es relativamente fácil: es muy probable que tus gustos no cambien demasiado incluso después de 10 años, y como
nes pequeño, se pueden ignorar problemas pequeños donde, aun existiendo una funciónO(n), usas unaO(n^2).Pero si es software que usarán otras personas, los requisitos son distintos, y con un
Nsuficientemente grande una funciónO(n)empieza a valer la pena, y así aparecen los problemas.Ya sea que lo escribas para ti o para otros, pueden surgir problemas imprevistos.
Por ejemplo, puede crashear al procesar archivos de más de 1 GB, pero como normalmente solo usabas archivos de menos de 100 KB no te importaba; y al intentar arreglarlo podrías descubrir que tienes que reescribir la mitad.
Ahí está la mayor objeción a la idea de que el software que no cambia sea intrínsecamente mejor que el software que cambia con frecuencia.
Porque puede que un software que no cambia haya sido perfecto desde el principio, pero también puede esconder horrores en lo profundo, y es difícil distinguirlo de antemano.
Tampoco significa que el software que se actualiza rápido sea intrínsecamente mejor que el que se actualiza lento; hay muchos factores además de la velocidad de actualización.
Si los requisitos cambian, por supuesto que el software también debe cambiar.
Pero en 10 años pueden pasar muchas cosas que no tienen nada que ver con cambios de requisitos.
Un proyecto open source puede ser abandonado o cambiar de rumbo, un software comercial puede descontinuarse, una empresa puede ser adquirida, pueden cambiar las reglas de la App Store o Play Store, una API puede desaparecer o cambiar de precio y destruir la viabilidad económica de un proyecto.
También cambian las toolchains, los frameworks, los lenguajes de programación, los paradigmas y las mejores prácticas.
Creo que el punto central es evitar que cambios externos ajenos a los requisitos me obliguen a cambiar.
Es un buen principio, pero como siempre, hay compromisos.
Ser estable y quedarse obsoleto son cosas distintas, y esa diferencia a menudo se define en la seguridad.
¿Qué hacer si cumplir un nuevo requisito importante es fácil, pero para eso hay que subir una librería vendorizada siete versiones mayores, y eso provoca un montón de roturas no relacionadas?
¿Qué hacer si ya no hay suficiente gente familiarizada con un conjunto de herramientas detenido en el tiempo y nadie quiere aprenderlo?
Elegir dependencias con cuidado y de forma conservadora está bien, pero creo que no seguir ni siquiera esos cambios de dependencias que mantuviste tan reducidos es ir un paso demasiado lejos.
Simpatizo con el tono del artículo.
Detesto de verdad que incluso una app móvil hecha hace apenas unos años ahora requiera decenas de horas para parchearla y enviar una actualización.
También me pareció interesante la última parte, donde el autor llama a su generador de sitios estáticos software de sangre fría y dice que corre en Python 2.
Python 2 se está volviendo cada vez más difícil de instalar hoy en día, y al final ese proyecto también se convertirá en un proyecto de sangre caliente.
Cada vez que actualizo Xcode tengo que arreglar cositas para que el proyecto compile limpio y funcione; es realmente irritante y debería ser completamente inaceptable.
Los mensajes recientes del historial de git son todos variaciones de “corregido para que funcione en el Xcode más reciente”.
Si estos cambios en SDKs u OS anteriores fueran necesarios por amenazas de seguridad, podría entenderlo hasta cierto punto, pero casi nunca es el caso.
La mayoría son cambios tontos como deprecar APIs, agregar advertencias por defecto o decir que ahora hay que usar este framework en vez de aquel.
Las plataformas y frameworks deberían dejar de convertirse deliberadamente en objetivos móviles, especialmente si ya son sistemas operativos muy estables y confiables.
Uno debería poder sacar un proyecto de 10 años del congelador y que compile y se ejecute tan limpiamente como hace 10 años.
Estos proveedores de sistemas operativos son empresas de billones, así que no quiero oír la excusa de que la compatibilidad hacia atrás requiere mucho esfuerzo de ingeniería.
Sigo manteniendo un proyecto personal secundario.
Empezó hace 12 o 13 años en PHP puro; luego lo reescribí en Laravel y alrededor de 2017 lo volví a reescribir en Symfony.
Hubo periodos de 6 a 18 meses en los que, por trabajar full time como freelancer y no tener energía, hice apenas 2 o 3 commits muy pequeños; pero cuando tenía tiempo agregaba funciones, actualizaba, experimentaba y aprendía.
Me resultó muy útil para aprender a mantener un proyecto a largo plazo.
Aprendí cosas como actualizar dependencias, eliminar lo innecesario, revisar actualizaciones de seguridad, buscar oportunidades de simplificación (de Vagrant a Docker, de Vue + Axios + Webpack y similares a Htmx), y también aprendí qué evitar.
Personalmente, terminé evitando dependencias recién salidas del horno, microservicios e infraestructura compleja como Kubernetes.
Últimamente desarrollé varias funciones, lo subí a PHP 8.2 y Symfony 7, e integré una función basada en ChatGPT, así que creo que, si quiero, podría descansar entre 1 y 3 años.
En los últimos 4 o 5 años este proyecto generó ingresos parecidos al ingreso anual promedio de un freelancer, así que tampoco es un proyecto secundario desconocido y dormido.
Volví después de no usarlo durante varios años, y las mismas funciones horribles de manipulación de imágenes que existían cuando me fui hace 8 años seguían ahí intactas.
Además de lo que dice el artículo, también es importante tener un modelo de amenazas inherentemente seguro.
Por ejemplo, un sitio web completo tiene que lidiar constantemente con atacantes y bots de spam, así que por naturaleza se acerca más a la sangre caliente.
En cambio, una página estática como TiddlyWiki ni siquiera necesita publicarse en la web, y el navegador es una plataforma tremendamente estable, así que es mucho mejor.
La diferencia de preferencias entre proyectos de sangre fría y proyectos de sangre caliente parece estar relacionada con el Buxton Index que aparece en https://www.cs.utexas.edu/users/EWD/transcriptions/EWD11xx/EWD1175.html
Una pequeña tienda de abarrotes de barrio tiene alrededor de 0.5 años; un verdadero cristiano, infinito; un político promedio que busca la reelección, unos 4 años; la mayoría de las industrias, un poco más; y los gerentes que tienen que escribir reportes trimestrales, mucho menos.
La razón por la que el Buxton Index es importante es que la colaboración estrecha entre entidades con Buxton Index muy distintos necesariamente fracasa y termina en reproches morales.
Al lado de corto plazo se le acusa de superficial y miope, y al de largo plazo se le acusa de negligencia laboral, evasión de responsabilidades o de aprovecharse sin aportar.
También llegan a considerarse tontos mutuamente.
La ventaja del Buxton Index es que, al ser un concepto numérico simple, es moralmente neutral y eleva la diferencia por encima de la discusión moral.
Es especialmente importante al pensar en la colaboración entre la academia y la industria.
Este nombre es realmente malo.
Los animales de sangre fría dependen mucho del entorno, y los de sangre caliente reducen su dependencia de la temperatura externa mediante el metabolismo.
En todo caso, es innecesariamente ambiguo.
Si simplemente dijeran “software sin dependencias externas”, podrían eliminar ese párrafo explicativo tan largo.
No me gustan los artículos de desarrollo de software que sacan conclusiones superficiales a partir de analogías inadecuadas tomadas de la naturaleza, pero me gustan todavía menos los que lo hacen entendiendo completamente mal el propio fenómeno natural al que aluden.
Que algunas especies, incluida la tortuga pintada, sobrevivan a la congelación no se debe a que sean de sangre fría, sino a proteínas anticongelantes especiales.
Otros lagartos o animales de sangre fría verían sus propios tejidos romperse al descongelarse.
https://lobste.rs/s/hitos3/cold_blooded_software#c_mxjzwh