Desarrollo de software de bajo costo de mantenimiento

Objetivos

  • Cero deuda técnica.
  • El diseño del software es fácil de entender; cuando se requiere modificar el software, es muy sencillo localizar el código correspondiente.
  • El software tiene la mantenibilidad como su máxima prioridad; los desarrolladores pueden regresar a mejorarlo años después de haberlo olvidado.
  • El software está infinitamente cerca de completarse, manteniéndose satisfactorio hasta que quede obsoleto debido a cambios en el entorno o surjan nuevos requisitos.
  • Se pueden actualizar con confianza las dependencias que comparten la misma versión principal.
  • Cuando el software queda en desuso, puede eliminarse sin problemas del sistema existente.

Principios

Recuerda que «no hay almuerzo gratis»: es imposible obtener un proyecto de software de bajo costo de mantenimiento sin hacer nada. La clave para lograr un desarrollo de software de bajo costo de mantenimiento no radica en cometer unos pocos errores menos, sino en hacer bien cada pequeño detalle en la medida de lo posible, lo cual es indudablemente muy difícil.
A continuación, se resumen algunos principios generales que ayudan a lograr un desarrollo de software de bajo costo de mantenimiento:
  • Descartar todas las características no esenciales.
  • Dejar de utilizar lenguajes y cadenas de herramientas con ecosistemas poco saludables.
  • Usar lenguajes con gran poder expresivo, evitando distorsionar o desviarse de los patrones comunes del lenguaje durante la codificación.
  • Dejar de usar macros para cualquier propósito que pueda generar malos olores en el código.
  • Usar únicamente lenguajes con definiciones o anotaciones de tipos.
  • Utilizar en la medida de lo posible tecnologías probadas por el tiempo, ya que es de esperar que sobrevivan por más tiempo.
  • Pensar en los problemas de manera global y evitar la programación táctica.
  • Refactorizar activamente el código, permitiendo la modificación de grandes volúmenes de líneas para reducir la entropía del software.
  • Eliminar diseños prescindibles trasladando la complejidad a los usuarios, ya que los diseños excesivamente complacientes suelen ser los más difíciles de mantener. Existen muchos ejemplos similares en la realidad. Consideremos los editores Markdown y los editores WYSIWYG: Los primeros requieren un costo de aprendizaje (mínimo) por parte del usuario, mientras que los segundos casi no exigen ninguno; sin embargo, estos últimos son mucho peores en términos de mantenibilidad:
    • Los editores Markdown facilitan la redacción de archivos de texto plano estructurados y legibles por humanos. Los editores WYSIWYG producen modelos de documentos internos difíciles de migrar o un HTML sumido en el caos absoluto.
    • Los documentos Markdown seguirán siendo útiles en 2050; los editores Markdown suelen ser minimalistas y reemplazables. Los editores WYSIWYG quedan obsoletos con mayor facilidad, mantener su compatibilidad hacia atrás es más difícil y resultan más complejos de mantener para los desarrolladores; es muy probable que su proveedor ya no exista en 2050.
  • Dejar de usar marcos de trabajo (frameworks) masivos que puedan provocar el bloqueo del proveedor (vendor lock-in). Cuando un marco de trabajo intenta adaptarse a todos los entornos o pretende ofrecer una solución integral, deja de ser un marco de software para convertirse en una bomba de tiempo.
  • Reducir al mínimo la dependencia de paquetes de software de baja calidad. Esto implica realizar revisiones de calidad del código de las dependencias; afortunadamente, los malos estilos de codificación son fáciles de identificar y rara vez se requiere comprender a fondo el funcionamiento interno del paquete para juzgarlo.
  • No escribir software para plataformas que carezcan de compatibilidad hacia atrás.
  • A menos que el software tenga una única función, se debe evitar en la medida de lo posible que sea multiplataforma.
  • Prohibir el uso de servicios propietarios ofrecidos por plataformas de computación en la nube.
  • Prohibir el uso de gestores de paquetes que no puedan bloquear las versiones de las subdependencias, y prohibir el uso de repositorios Git como fuentes de paquetes de software.
  • A menos que sea necesario, no ceder ante estándares y protocolos existentes:
    • La mayoría de los estándares y protocolos son basura; gran parte de ellos ni siquiera tiene valor de referencia, y amoldarse a ellos solo retrasará el desarrollo del software. Por ejemplo, podrías encontrarte con el infortunio de tener requisitos imposibles de implementar con dichos estándares y protocolos, quedando atrapado en un dilema.
    • Aquellos estándares y protocolos tan populares que no pueden ser ignorados deben su popularidad mayormente a su ventaja de ser pioneros y a la garantía de compatibilidad hacia atrás. Esto hace que sus implementaciones en producción estén plagadas de casos extremos extraños; recopilar y lidiar con estos casos extremos resulta agotador.
  • Prestar atención al nivel de abstracción de la cadena de herramientas y dejar de utilizar aquellas difíciles de migrar o actualizar.
  • Escribir una gran cantidad de pruebas de integración automatizadas para aumentar la seguridad al actualizar dependencias.
  • El entorno necesario para ejecutar el software debe ser dedicado y aislado, no compartido con otros proyectos.
  • Valorar la soberanía tecnológica y desvincularse tanto como sea posible de dependencias susceptibles a factores externos.
  • Reducir al mínimo el número de desarrolladores fuera del equipo central para evitar que las fricciones colaborativas perjudiquen la eficiencia del desarrollo del proyecto. Esto es como el tráfico vehicular: la mejor solución no es ensanchar las calles, sino reducir la cantidad de vehículos circulando simultáneamente.

Reinventar la rueda

Reinventar la rueda se considera un antipatrón porque siempre hay una o más ruedas ya inventadas esperando a ser reutilizadas. Sin embargo, la reutilización de ruedas existentes a menudo desencadena problemas de gestión de dependencias: todo marcha bien al principio, pero los problemas no tardan en sucederse. El camino más fácil suele ser el que maximiza la deuda técnica. Transitar por esa senda a menudo genera frustración. Con el tiempo, sin importar cuánta confianza y pasión se tenga, sobreviene la indefensión aprendida, considerando el desarrollo de software como una labor ardua sin fin a la vista.
Juzgar si vale la pena introducir una dependencia es una habilidad crucial que suele pasarse por alto y que requiere el respaldo de una vasta experiencia tácita. Al carecer de esta habilidad, es fácil ser engañado, convirtiéndose en una víctima futura o incluso en cómplice de los perpetradores. Dado que la mayoría de los desarrolladores carece de esta capacidad, el fenómeno de la moneda mala expulsando a la buena es sumamente común en la industria.
Los proyectos de software de código abierto que alcanzan cierta escala se convierten fácilmente en monolitos caóticos (codebases deplorables):
  • Abstracciones erróneas, arquitecturas deficientes y una superficie de API aterradora.
  • Falta de pruebas convincentes.
  • Documentación descuidada, inconsistente con la realidad, o que encubre u omite detalles útiles.
  • Exceso de programación táctica, escasez de programación estratégica; el volumen de código nuevo siempre supera al código eliminado.
  • Demasiado tiempo invertido en requisitos inexistentes, provocando que el software se infle y se pudra.
  • ...
A menos que en la comunidad existan mantenedores con autoridad que supervisen con una perspectiva estratégica y apliquen revisiones de código rigurosas, el modelo de participación del software de código abierto generará intrínsecamente este tipo de desastres, sin importar cuán capacitados sean los colaboradores. El software de código cerrado padece el mismo problema; su opacidad simplemente oculta aún más estas deficiencias. Barrer los problemas bajo la alfombra no significa que hayan desaparecido.
Para liberarse de la deuda técnica, a veces no queda más remedio que colocarse el sombrero del NIH. Al reinventar la rueda, podrías notar que, más allá de librarnos de pésimas dependencias de terceros, esta práctica ofrece beneficios que los desarrolladores abrumados por la deuda técnica jamás podrán conocer:
  • Existe la posibilidad de inventar una rueda mejor.
  • Te volverás cada vez más hábil inventando soluciones.
  • Desarrollarás una mayor capacidad para discernir entre dependencias buenas y malas.
Cuando tu objetivo es un bajo costo de mantenimiento, los rendimientos de reinventar la rueda pueden multiplicarse aún más en el futuro: una vez saldada la deuda, todo lo que queda son intereses; un principio así de simple. Con el tiempo, la acumulación de esta retroalimentación positiva hará que la opción de reinventar la rueda ocupe un lugar cada vez más importante en tu criterio. No obstante, antes de reinventar la rueda, siempre se debe evaluar prudentemente su viabilidad económica. Reinventarlo todo sigue siendo desaconsejable; resulta evidente que hay ruedas cuya reinvención tiene una baja relación costo-beneficio y un alto riesgo de fracaso, por lo que debes ser cauteloso.

Construir dependencias tipo batería

Algunos lenguajes de programación y marcos de trabajo afirman venir «con baterías incluidas» (batteries included), refiriéndose a que incorporan funciones avanzadas listas para usar. La esencia de estas «baterías» radica en ser APIs de alto nivel relativamente estables que evitan a toda costa exponer detalles de bajo nivel; sus abstracciones y pruebas correctas hacen que rara vez sufran cambios incompatibles.
El concepto de «batería» puede aplicarse perfectamente a la gestión de dependencias, creando lo que podríamos llamar una «dependencia tipo batería»: Esta dependencia actúa como una capa intermedia entre el proyecto que la utiliza y la dependencia externa, ofreciendo una API ordenada y limpia basada en dicha dependencia, lo que evita que el código de negocio consuma directamente funciones o clases monstruosas o con cantidades absurdas de parámetros diseñadas por bárbaros.
Puesto que solo te enfocas en las partes utilizadas de dichas dependencias, puedes escribir tus propias pruebas en la capa intermedia para asegurar que, al actualizar la versión de la dependencia, su comportamiento siga cumpliendo las expectativas. Si en el futuro la dependencia deja de funcionar, puedes optar por mantener una versión antigua, cambiarla por otra o implementarla tú mismo; las pruebas escritas previamente servirán para garantizar la validez durante la refactorización. Esta es la diferencia clave entre las baterías construidas por uno mismo y las construidas por terceros: a tu propia batería siempre puedes recargarla o ampliar su capacidad, mientras que con la ajena, usualmente solo te queda ver cómo se agota lentamente.
Debido a su similitud formal, es fácil confundir una dependencia tipo batería con lógica de negocio encapsulada; la diferencia principal radica en su distancia respecto a la dependencia. Una dependencia tipo batería suele estar muy cerca de la dependencia externa, mientras que la lógica de negocio suele estar muy lejos de ella. Solo en contadas ocasiones, como cuando la lógica de negocio se fundamenta directamente en las dependencias asociadas, ambas tienden a converger.
La dependencia tipo batería parece el santo grial para resolver los problemas de gestión de dependencias, pero aún presenta algunos inconvenientes:
  • Al fin y al cabo, es una capa intermedia adicional; requerir una nueva funcionalidad o realizar cambios incompatibles resultará más engorroso que consumir la dependencia de forma directa.
  • Construir abstracciones correctas es difícil. En el camino hacia la abstracción adecuada, a veces es inevitable tener que empezar de cero. A menudo también se carece de previsión sobre los requisitos futuros, por lo que una abstracción correcta en el presente puede tornarse incorrecta más adelante. La gente teme que el proyecto caiga en un ciclo de modificaciones constantes debido a abstracciones erróneas, prefiriendo sumergirse en un vórtice mayor; al menos culpar a otros cuando algo falla siempre es más fácil, y es difícil rebatir esa postura.

Utilizar paquetes pequeños

Los paquetes pequeños son la manifestación conjunta de la filosofía Unix y el control de versiones semántico. Cuanto menor sea la granularidad del paquete, menos funciones tendrá el paquete individual; a menor número de funciones, mayor estabilidad, y menos cosas se romperán al modificar el código.
A veces, los paquetes pequeños traen consigo nuevos problemas, como las dificultades de gestión derivadas de la proliferación de paquetes. Este tipo de problemas puede volverse intrincado, ya que con el tiempo se vuelve cada vez más difícil de resolver mediante la fuerza bruta. Dicha tendencia terminará forzando la aparición de soluciones dedicadas, como herramientas del estilo de codemod de Facebook, aunque recurrir a ellas sigue siendo engorroso.

Elegir legibilidad sobre rendimiento

En la codificación real, se presentan situaciones de indecisión entre legibilidad y rendimiento; en esos momentos, es fácil que el desarrollador opte por el rendimiento debido a una miopía temporal. Elegir el rendimiento no suele estar respaldado por motivos de peso, y sacrificar legibilidad a cambio de rendimiento rara vez resulta rentable retrospectivamente. En algunos casos, los desarrolladores eligen el rendimiento únicamente por el beneficio psicológico pueril de sentir que «escribir código de alto rendimiento me hace ver inteligente».
Cuando vaciles entre legibilidad y rendimiento, opta siempre por la legibilidad; esto nunca se enfatizará lo suficiente. ¿Estás completamente seguro de que este código solo se escribirá una vez y jamás volverá a modificarse en la vida? Elige legibilidad. ¿El código más legible es 1000 veces más lento? Elige legibilidad. Si realmente se produce un impacto negativo observable en el rendimiento, escribe entonces una segunda versión del código: una versión de alto rendimiento transpilada a partir del código de alta legibilidad, acompañada de documentación sumamente detallada que explique cómo se originó ese código.
Una excepción ocurre cuando necesitas escribir programas de alto rendimiento de forma estricta; dado que tu objetivo inicial era el alto rendimiento, la legibilidad deja de ser una opción en ese punto. Sin embargo, incluso en programas de alto rendimiento, aún existen oportunidades para mejorar la legibilidad del código, solo que tales oportunidades son difíciles de percibir: a menudo solo se notan al escribir el código por segunda vez. Y una vez detectadas esas oportunidades para mejorar la legibilidad, no hay motivo para seguir ignorándolas: manteniendo el rendimiento intacto, siempre se debe maximizar la legibilidad en la medida de lo posible.

Apreciar el software libre de mantenimiento

La cualidad de no requerir mantenimiento en un software debe apreciarse como una forma de belleza. Cuanto mayor sea el tiempo que un software pasa sin requerir mantenimiento, más tiempo habrá superado la prueba dicha cualidad y más belleza habrá acumulado.
El aspecto crucial radica en que sabes con certeza que el software no es imposible ni difícil de mantener, sino que simplemente no necesita mantenimiento. El código está ahí, pero no tienes que manipularlo porque sigue satisfaciendo las necesidades, funcionando normalmente y haciéndolo de maravilla.
Incluso cuando llega el momento del mantenimiento, la resistencia debe ser tan frágil como el papel de una ventana, disipándose con un leve toque. Al no haber deuda técnica, no tienes que pagar intereses. ¿Te da un ataque de obsesión y quieres actualizar dependencias? No hay problema, porque esa actualización puede completarse en diez minutos, y tienes muchos bloques de diez minutos para gastar. Pero aún mejor es vencerte a ti mismo y frenar tu impulso a tiempo: no solo porque no hayan surgido nuevos requisitos, sino porque no deseas arruinar la belleza que el software ha acumulado a lo largo del tiempo.
Desarrollar esta capacidad de apreciación es bastante difícil porque resulta contraintuitiva. La gente tiende a considerar inconscientemente la novedad de las cosas como una medida errónea de su calidad; mantenerse a la moda a veces parece más importante que cualquier otra cosa, pero un mínimo de reflexión revela que esto no es racional. En la vida cotidiana se comprueba que los aparatos con materiales sólidos y mano de obra madura suelen ser sorprendentemente duraderos: un refrigerador, siendo un electrodoméstico de funcionamiento ininterrumpido, es el ejemplo arquetípico; casi nadie cambia de refrigerador cada año. El software, como producto digital, no tiene por qué volverse obsoleto más rápido que el hardware; la facilidad para actualizar jamás debería ser la razón para hacerlo.