Capítulo 1. ¿Qué es la ingeniería del software? ¿Qué es la ingeniería de software?

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Nada se construye sobre piedra; todo se construye sobre arena, pero debemos construir como si la arena fuera piedra.

Jorge Luis Borges

Vemos tres diferencias críticas entre la programación y la ingeniería de software: el tiempo, la escala y las compensaciones en juego. En un proyecto de ingeniería de software, los ingenieros deben preocuparse más por el paso del tiempo y la eventual necesidad de cambio. En una organización de ingeniería de software, debemos preocuparnos más por la escala y la eficacia, tanto del software que producimos como de la organización que lo produce. Por último, como ingenieros de software, se nos pide que tomemos decisiones más complejas con resultados más arriesgados, a menudo basados en estimaciones imprecisas de tiempo y crecimiento.

En Google decimos a veces: "La ingeniería de software es la programación integrada en el tiempo". Programar es sin duda una parte importante de la ingeniería de software: al fin y al cabo, programando es como se genera nuevo software en primer lugar. Si aceptas esta distinción, también queda claro que podríamos necesitar delimitar entre las tareas de programación (desarrollo) y las tareas de ingeniería de software (desarrollo, modificación, mantenimiento). La adición del tiempo añade una nueva dimensión importante a la programación. Los cubos no son cuadrados, la distancia no es velocidad. La ingeniería de software no es programación.

Una forma de ver el impacto del tiempo en un programa es pensar en la pregunta: "¿Cuál es la vida útil prevista1 de tu código?" Las respuestas razonables a esta pregunta varían aproximadamente en un factor de 100.000. Es tan razonable pensar en un código que debe durar unos minutos como imaginar un código que vivirá décadas. Generalmente, el código en el extremo corto de ese espectro no se ve afectado por el tiempo. Es poco probable que necesites adaptarte a una nueva versión de tus bibliotecas subyacentes, sistema operativo (SO), hardware o versión del lenguaje para un programa cuya utilidad dure sólo una hora. Estos sistemas efímeros son efectivamente "sólo" un problema de programación, del mismo modo que un cubo comprimido lo suficiente en una dimensión es un cuadrado. A medida que ampliamos ese tiempo para permitir períodos de vida más largos, el cambio adquiere mayor importancia. A lo largo de una década o más, la mayoría de las dependencias del programa, ya sean implícitas o explícitas, probablemente cambiarán. Este reconocimiento está en la raíz de nuestra distinción entre ingeniería de software y programación.

Esta distinción es el núcleo de lo que llamamos sostenibilidad del software. Tu proyecto es sostenible si, durante la vida útil prevista de tu software, eres capaz de reaccionar ante cualquier cambio valioso que se produzca, ya sea por motivos técnicos o empresariales. Es importante destacar que sólo buscamos la capacidad: puedes decidir no realizar una actualización determinada, ya sea por falta de valor o por otras prioridades.2 Cuando eres fundamentalmente incapaz de reaccionar ante un cambio en la tecnología subyacente o en la dirección del producto, estás haciendo una apuesta de alto riesgo con la esperanza de que ese cambio nunca llegue a ser crítico. Para proyectos a corto plazo, puede ser una apuesta segura. A lo largo de varias décadas, probablemente no lo sea.3

Otra forma de ver la ingeniería de software es considerar la escala. ¿Cuántas personas participan? ¿Qué papel desempeñan en el desarrollo y el mantenimiento a lo largo del tiempo? Una tarea de programación suele ser un acto de creación individual, pero una tarea de ingeniería de software es un esfuerzo de equipo. Un primer intento de definir la ingeniería de software produjo una buena definición para este punto de vista: "El desarrollo multipersonal de programas multiversión".4 Esto sugiere que la diferencia entre la ingeniería de software y la programación es tanto de tiempo como de personas. La colaboración en equipo presenta nuevos problemas, pero también proporciona más potencial para producir sistemas valiosos de lo que podría hacerlo un solo programador.

La organización del equipo, la composición del proyecto y las políticas y prácticas de un proyecto de software dominan este aspecto de la complejidad de la ingeniería de software. Estos problemas son inherentes a la escala: a medida que la organización crece y sus proyectos se amplían, ¿se vuelve más eficiente en la producción de software? ¿Nuestro flujo de trabajo de desarrollo se hace más eficiente a medida que crecemos, o nuestras políticas de control de versiones y estrategias de prueba nos cuestan proporcionalmente más? Las cuestiones de escala en torno a la comunicación y el escalado humano se han debatido desde los primeros días de la ingeniería de software, remontándose hasta el Mes del Hombre Mítico.5 Estas cuestiones de escala suelen ser asuntos de política y son fundamentales para la cuestión de la sostenibilidad del software: ¿cuánto costará hacer las cosas que necesitamos hacer repetidamente?

También podemos decir que la ingeniería de software es diferente de la programación en cuanto a la complejidad de las decisiones que hay que tomar y lo que está en juego. En la ingeniería de software, nos vemos obligados regularmente a evaluar las compensaciones entre varios caminos a seguir, a veces con mucho en juego y a menudo con métricas de valor imperfectas. El trabajo de un ingeniero de software, o de un líder de ingeniería de software, es aspirar a la sostenibilidad y la gestión de los costes de escalado para la organización, el producto y el flujo de trabajo de desarrollo. Teniendo en cuenta esas aportaciones, evalúa tus compensaciones y toma decisiones racionales. A veces podemos aplazar los cambios de mantenimiento, o incluso adoptar políticas que no escalan bien, sabiendo que tendremos que revisar esas decisiones. Esas decisiones deben ser explícitas y claras en cuanto a los costes aplazados.

Rara vez existe una solución única en ingeniería de software, y lo mismo puede decirse de este libro. Teniendo en cuenta un factor de 100.000 para las respuestas razonables sobre "¿Cuánto tiempo vivirá este software?", un rango de quizás un factor de 10.000 para "¿Cuántos ingenieros hay en tu organización?" y quién sabe cuántos para "¿Cuántos recursos informáticos hay disponibles para tu proyecto?", la experiencia de Google probablemente no coincidirá con la tuya. En este libro, pretendemos presentar lo que hemos descubierto que nos funciona en la construcción y el mantenimiento de software que esperamos que dure décadas, con decenas de miles de ingenieros y recursos informáticos en todo el mundo. La mayoría de las prácticas que nos parecen necesarias a esa escala también funcionarán bien para empresas más pequeñas: considera esto un informe sobre un ecosistema de ingeniería que creemos que podría ser bueno a medida que se amplía. En algunos lugares, la escala supergrande conlleva sus propios costes, y nos alegraría no tener que pagar gastos generales adicionales. Los señalamos como advertencia. Esperamos que si tu organización crece lo suficiente como para preocuparte por esos costes, puedas encontrar una respuesta mejor.

Antes de entrar en detalles sobre el trabajo en equipo, la cultura, las políticas y las herramientas, profundicemos en los temas principales del tiempo, la escala y las compensaciones.

Tiempo y cambio

Cuando un principiante está aprendiendo a programar, la vida útil del código resultante suele medirse en horas o días. Las tareas y ejercicios de programación tienden a escribirse una sola vez, sin apenas refactorización y, desde luego, sin mantenimiento a largo plazo. Estos programas no suelen reconstruirse ni ejecutarse nunca más tras su producción inicial. Esto no es sorprendente en un entorno pedagógico. Quizá en la enseñanza secundaria o postsecundaria encontremos un curso de proyectos en equipo o una tesis práctica. Si es así, es probable que esos proyectos sean el único momento en que el código de los estudiantes viva más de un mes o así. Puede que esos desarrolladores necesiten refactorizar algo de código, tal vez como respuesta a requisitos cambiantes, pero es poco probable que se les pida que se ocupen de cambios más amplios en su entorno.

También encontramos desarrolladores de código efímero en entornos industriales comunes. Las aplicaciones móviles suelen tener una vida bastante corta,6 y para bien o para mal, las reescrituras completas son relativamente habituales. Los ingenieros de una startup en fase inicial podrían optar, con razón, por centrarse en objetivos inmediatos antes que en inversiones a largo plazo: la empresa podría no vivir lo suficiente como para cosechar los beneficios de una inversión en infraestructura que se amortiza lentamente. Un desarrollador de una startup en serie podría muy razonablemente tener 10 años de experiencia en desarrollo y poca o ninguna en el mantenimiento de cualquier pieza de software que se espere que exista durante más de uno o dos años.

En el otro extremo del espectro, algunos proyectos de éxito tienen una vida útil efectivamente ilimitada: no podemos predecir razonablemente un final para la Búsqueda de Google, el núcleo de Linux o el proyecto del Servidor HTTP Apache. Para la mayoría de los proyectos de Google, debemos suponer que vivirán indefinidamente: no podemos predecir cuándo no necesitaremos actualizar las dependencias, las versiones del lenguaje, etc. A medida que aumentan su vida útil , estos proyectos de larga duración acaban teniendo una sensación diferente a la de las tareas de programación o el desarrollo de startups.

Considera la Figura 1-1, que muestra dos proyectos de software en extremos opuestos de este espectro de "vida útil prevista". Para un programador que trabaja en una tarea con una vida útil prevista de horas, ¿qué tipos de mantenimiento es razonable esperar? Es decir, si sale una nueva versión de tu sistema operativo mientras estás trabajando en un script de Python que se ejecutará una sola vez, ¿deberías dejar lo que estás haciendo y actualizarlo? Por supuesto que no: la actualización no es crítica. Pero en el extremo opuesto del espectro, que la Búsqueda de Google esté atascada en una versión de nuestro SO de los años 90 sería un claro problema.

Life span and the importance of upgrades
Figura 1-1. Vida útil e importancia de las actualizaciones

Los puntos bajos y altos del espectro de la vida útil prevista sugieren que hay una transición en alguna parte. En algún punto de la línea entre un programa puntual y un proyecto que dura décadas, se produce una transición: un proyecto debe empezar a reaccionar ante los cambios de las externalidades.7 Para cualquier proyecto que no haya planificado las actualizaciones desde el principio, es probable que esa transición sea muy dolorosa por tres razones, cada una de las cuales agrava las otras:

  • Estás realizando una tarea que aún no se ha hecho para este proyecto; se han incorporado más supuestos ocultos.

  • Es menos probable que los ingenieros que intentan hacer la actualización tengan experiencia en este tipo de tareas.

  • El tamaño de la actualización suele ser mayor de lo habitual, haciendo de una vez actualizaciones de varios años en lugar de una actualización más incremental.

Y así, después de pasar realmente por una actualización de este tipo una vez (o abandonar a mitad de camino), es bastante razonable sobrestimar el coste de hacer una actualización posterior y decidir "Nunca más". Las empresas que llegan a esta conclusión acaban comprometiéndose a simplemente tirar cosas y reescribir su código, o deciden no volver a actualizar. En lugar de adoptar el enfoque natural evitando una tarea dolorosa, a veces la respuesta más responsable es invertir en hacerla menos dolorosa. Todo depende del coste de tu actualización, del valor que aporte y de la vida útil prevista del proyecto en cuestión.

Superar no sólo esa primera gran actualización, sino llegar al punto en el que puedas mantenerte actualizado de forma fiable en el futuro, es la esencia de la sostenibilidad a largo plazo de tu proyecto. La sostenibilidad requiere planificar y gestionar el impacto del cambio necesario. En muchos proyectos de Google, creemos que hemos logrado este tipo de sostenibilidad, en gran medida a base de ensayo y error.

Entonces, concretamente, ¿en qué se diferencia la programación a corto plazo de la producción de código con una vida útil prevista mucho más larga? Con el tiempo, tenemos que ser mucho más conscientes de la diferencia entre "resulta que funciona" y "es mantenible". No existe una solución perfecta para identificar estas cuestiones. Es una lástima, porque mantener el software mantenible a largo plazo es una batalla constante.

Ley de Hyrum

Si estás manteniendo un proyecto de que utilizan otros ingenieros de , la lección más importante sobre "funciona" frente a "es mantenible" es lo que hemos dado en llamar la Ley de Hyrum:

Con un número suficiente de usuarios de una API, no importa lo que prometas en el contrato: todos los comportamientos observables de tu sistema dependerán de alguien.

Según nuestra experiencia, este axioma es un factor dominante en cualquier debate sobre el cambio de software a lo largo del tiempo. Es conceptualmente similar a la entropía: los debates sobre el cambio y el mantenimiento a lo largo del tiempo deben tener en cuenta la Ley de Hyrum8 igual que las discusiones sobre eficiencia o termodinámica deben tener presente la entropía. Que la entropía nunca disminuya no significa que no debamos intentar ser eficientes. Que la Ley de Hyrum se aplique al mantenimiento del software no significa que no podamos planificarla o intentar comprenderla mejor. Podemos mitigarla, pero sabemos que nunca podrá erradicarse.

La Ley de Hyrum representa el conocimiento práctico de que -incluso con la mejor de las intenciones, los mejores ingenieros y unas prácticas sólidas de revisión del código- no podemos asumir una adhesión perfecta a los contratos publicados o a las buenas prácticas. Como propietario de una API, ganarás cierta flexibilidad y libertad siendo claro sobre las promesas de la interfaz, pero en la práctica, la complejidad y dificultad de un cambio determinado también depende de lo útil que un usuario encuentre algún comportamiento observable de tu API. Si los usuarios no pueden depender de esas cosas, tu API será fácil de cambiar. Con tiempo y usuarios suficientes , incluso el cambio más inocuo romperá algo;9 tu análisis del valor de ese cambio debe incorporar la dificultad de investigar, identificar y resolver esas roturas.

Ejemplo: Ordenación Hash

Considera el ejemplo del orden de iteración hash. Si introducimos cinco elementos en un conjunto basado en hash, ¿en qué orden los sacamos?

>>> for i in {"apple", "banana", "carrot", "durian", "eggplant"}: print(i)
... 
durian
carrot
apple
eggplant
banana

La mayoría de los programadores saben que las tablas hash están ordenadas de forma no obvia. Pocos conocen los detalles de si la tabla hash concreta que están utilizando pretende proporcionar ese orden particular para siempre. Esto puede parecer anodino, pero en la última década o dos, la experiencia de la industria informática en el uso de estos tipos ha evolucionado:

  • Inundación de hash10 proporcionan un mayor incentivo para la iteración hash no determinista.

  • Eficiencia potencial las ganancias de la investigación en algoritmos hash mejorados o contenedores hash requieren cambios en el orden de iteración hash.

  • Según la Ley de Hyrum, los programadores escribirán programas que dependan del orden en que se recorre una tabla hash, si tienen la capacidad de hacerlo.

Como resultado, si preguntas a cualquier experto "¿Puedo asumir una secuencia de salida concreta para mi contenedor de hash?", ese experto dirá presumiblemente "No". En general, eso es correcto, pero quizás simplista. Una respuesta más matizada es: "Si tu código es de corta duración, sin cambios en el hardware, el tiempo de ejecución del lenguaje o la elección de la estructura de datos, tal suposición está bien. Si no sabes cuánto tiempo vivirá tu código, o no puedes prometer que nada de lo que dependas cambiará nunca, tal suposición es incorrecta". Además, aunque tu propia implementación no dependa del orden de los contenedores hash, podría ser utilizada por otro código que implícitamente crea tal dependencia. Por ejemplo, si tu biblioteca serializa valores en una respuesta de Llamada a Procedimiento Remoto (RPC), el llamante de la RPC podría acabar dependiendo del orden de esos valores.

Éste es un ejemplo muy básico de la diferencia entre "funciona" y "es correcto". Para un programa de corta duración, depender del orden de iteración de tus contenedores no causará ningún problema técnico. Para un proyecto de ingeniería de software, en cambio, esa dependencia de un orden definido es un riesgo: con tiempo suficiente, algo hará que sea valioso cambiar ese orden de iteración. Ese valor puede manifestarse de varias formas, ya sea por eficiencia, seguridad o simplemente para preparar la estructura de datos para futuros cambios. Cuando ese valor se haga evidente, tendrás que sopesar las compensaciones entre ese valor y el dolor de quebrantar a tus desarrolladores o clientes.

Algunos lenguajes aleatorizan específicamente el ordenamiento hash entre versiones de bibliotecas o incluso entre ejecuciones del mismo programa, en un intento de evitar dependencias. Pero incluso esto sigue permitiendo algunas sorpresas de la Ley de Hyrum: hay código que utiliza el orden de iteración hash como generador ineficiente de números aleatorios. Eliminar ahora esa aleatoriedad rompería a esos usuarios. Al igual que la entropía aumenta en todos los sistemas termodinámicos, la Ley de Hyrum se aplica a todos los comportamientos observables.

Reflexionando sobre las diferencias entre el código escrito con mentalidad de "funciona ahora" y de "funciona indefinidamente", podemos extraer algunas relaciones claras. Si consideramos el código como un artefacto con un requisito de vida útil (muy) variable, podemos empezar a categorizar los estilos de programación: el código que depende de características frágiles e inéditas de sus dependencias es probable que se describa como "chapucero" o "ingenioso", mientras que el código que sigue las buenas prácticas y se ha planificado para el futuro es más probable que se describa como "limpio" y "mantenible". Ambas tienen su utilidad, pero la que elijas dependerá fundamentalmente de la vida útil prevista del código en cuestión. Nos hemos acostumbrado a decir: "Es programación si 'limpio' es un cumplido, pero es ingeniería de software si 'limpio' es una acusación".

¿Por qué no aspirar simplemente a que "nada cambie"?

En toda esta discusión sobre el tiempo y la necesidad de reaccionar ante el cambio está implícita la suposición de que el cambio puede ser necesario. ¿Lo es?

Como con todo lo demás en este libro, depende. Nos comprometeremos fácilmente a "Para la mayoría de los proyectos, en un período de tiempo suficientemente largo, puede que sea necesario cambiar todo lo que hay debajo de ellos". Si tienes un proyecto escrito en C puro sin dependencias externas (o sólo dependencias externas que prometan una gran estabilidad a largo plazo, como POSIX), es muy posible que puedas evitar cualquier forma de refactorización o actualización difícil. C hace un gran trabajo proporcionando estabilidad; en muchos aspectos, ése es su principal propósito.

La mayoría de los proyectos están mucho más expuestos a los cambios de la tecnología subyacente. La mayoría de los lenguajes de programación y tiempos de ejecución cambian mucho más que C. Incluso las bibliotecas implementadas en C puro pueden cambiar para admitir nuevas funciones, lo que puede afectar a los usuarios posteriores. Los problemas de seguridad se revelan en todo tipo de tecnología, desde los procesadores a las bibliotecas de red, pasando por el código de las aplicaciones. Cada pieza de tecnología de la que depende tu proyecto tiene algún riesgo (esperemos que pequeño) de contener fallos críticos y vulnerabilidades de seguridad que podrían salir a la luz sólo después de que hayas empezado a depender de ella. Si eres incapaz de implementar un parche para Heartbleed o mitigar problemas de ejecución especulativa como Meltdown y Spectre porque has asumido (o prometido) que nada cambiará nunca, es una apuesta importante.

Las mejoras de la eficiencia complican aún más el panorama. Queremos equipar nuestros centros de datos con equipos informáticos rentables, especialmente mejorando la eficiencia de la CPU. Sin embargo, los algoritmos y las estructuras de datos de los primeros tiempos de Google son sencillamente menos eficientes en los equipos modernos: una lista enlazada o un árbol de búsqueda binario seguirán funcionando bien, pero la diferencia cada vez mayor entre los ciclos de la CPU y la latencia de la memoria afecta a lo que parece un código "eficiente". Con el tiempo, el valor de actualizar a un hardware más moderno puede disminuir sin que se produzcan cambios de diseño en el software. La compatibilidad con versiones anteriores garantiza que los sistemas antiguos sigan funcionando, pero eso no garantiza que las antiguas optimizaciones sigan siendo útiles. Si no se quiere o no se puede aprovechar estas oportunidades, se corre el riesgo de incurrir en grandes costes. Los problemas de eficiencia como éste son especialmente sutiles: el diseño original podría haber sido perfectamente lógico y seguir unas buenas prácticas razonables. Sólo después de una evolución de cambios retrocompatibles, una opción nueva y más eficiente adquiere importancia. No se cometieron errores, pero el paso del tiempo hizo que el cambio siguiera siendo valioso.

Preocupaciones como las que acabamos de mencionar son la razón por la que existen grandes riesgos para los proyectos a largo plazo que no han invertido en sostenibilidad. Debemos ser capaces de responder a este tipo de cuestiones y aprovechar estas oportunidades, independientemente de si nos afectan directamente o sólo se manifiestan en el cierre transitivo de la tecnología sobre la que construimos. El cambio no es intrínsecamente bueno. No debemos cambiar sólo por cambiar. Pero sí debemos ser capaces de cambiar. Si permitimos esa eventual necesidad, también deberíamos considerar si invertir en abaratar esa capacidad. Como sabe todo administrador de sistemas, una cosa es saber en teoría que puedes recuperarte de una cinta, y otra saber en la práctica cómo hacerlo exactamente y cuánto costará cuando sea necesario. La práctica y la experiencia son grandes impulsores de la eficacia y la fiabilidad.

Escala y eficiencia

Como se señala en el libro Site Reliability Engineering (SRE),11 el sistema de producción de Google en su conjunto es una de las máquinas más complejas creadas por la humanidad. La complejidad que entraña construir una máquina así y mantenerla funcionando sin problemas ha requerido incontables horas de reflexión, debate y rediseño por parte de expertos de toda nuestra organización y de todo el mundo. Así que ya hemos escrito un libro sobre la complejidad de mantener esa máquina en funcionamiento a esa escala.

Gran parte de este libro se centra en la complejidad de escala de la organización que produce dicha máquina, y en los procesos que utilizamos para mantener esa máquina en funcionamiento a lo largo del tiempo. Consideremos de nuevo el concepto de sostenibilidad de la base de código: "La base de código de tu organización es sostenible cuando eres capaz de cambiar todas las cosas que debes cambiar, de forma segura, y puedes hacerlo durante toda la vida de tu base de código". Oculto en el debate sobre la capacidad está también el de los costes: si cambiar algo supone un coste desmesurado, es probable que se aplace. Si los costes crecen superlinealmente con el tiempo, está claro que la operación no es escalable.12 Con el tiempo, el tiempo se impondrá y surgirá algo inesperado que deberás cambiar absolutamente. Cuando tu proyecto duplique su alcance y tengas que volver a realizar esa tarea, ¿será el doble de laboriosa? ¿Tendrás siquiera los recursos humanos necesarios para abordar el problema la próxima vez?

Los costes humanos no son el único recurso finito que necesita escalarse. Al igual que el propio software necesita escalar bien con recursos tradicionales como la computación, la memoria, el almacenamiento y el ancho de banda, el desarrollo de ese software también necesita escalar, tanto en términos de implicación de tiempo humano como de recursos de computación que impulsen tu flujo de trabajo de desarrollo. Si el coste informático de tu clúster de pruebas crece de forma superlineal, consumiendo más recursos informáticos por persona cada trimestre, estás en un camino insostenible y necesitas hacer cambios pronto.

Por último, el activo más preciado de una organización de software -el propio código base- también necesita escalar. Si tu sistema de compilación o de control de versiones escala de forma superlineal con el tiempo, quizá como resultado del crecimiento y del aumento del historial de cambios, puede llegar un punto en el que simplemente no puedas continuar. Muchas preguntas, como "¿Cuánto se tarda en hacer una compilación completa?", "¿Cuánto se tarda en extraer una copia nueva del repositorio?" o "¿Cuánto costará actualizar a una nueva versión del lenguaje?" no se monitorean activamente y cambian a un ritmo lento. Pueden llegar a ser fácilmente como la metafórica rana hervida; es demasiado fácil que los problemas empeoren lentamente y nunca se manifiesten como un momento singular de crisis. Sólo con una concienciación y un compromiso a escala de toda la organización es probable que te mantengas al tanto de estos problemas.

Todo aquello en lo que confía tu organización para producir y mantener el código debe ser escalable en términos de coste global y consumo de recursos. En particular, todo lo que tu organización debe hacer repetidamente debe ser escalable en términos de esfuerzo humano. Muchas políticas comunes no parecen ser escalables en este sentido.

Políticas que no se adaptan

Con un poco de práctica, resulta más fácil detectar las políticas con malas propiedades de escalado . Lo más habitual es identificarlas considerando el trabajo que se impone a un solo ingeniero e imaginando que la organización se escala 10 ó 100 veces. Cuando seamos 10 veces más grandes, ¿añadiremos 10 veces más trabajo con el que nuestro ingeniero de muestra tenga que seguir el ritmo? ¿La cantidad de trabajo que debe realizar nuestro ingeniero crece en función del tamaño de la organización? ¿El trabajo aumenta con el tamaño del código base? Si alguna de estas dos cosas es cierta, ¿disponemos de algún mecanismo para automatizar u optimizar ese trabajo? Si no es así, tenemos problemas de escalado.

Considera un enfoque tradicional de la depreciación. Hablaremos mucho más de la desaprobación en el Capítulo 15, pero el enfoque común de la desaprobación sirve como gran ejemplo de los problemas de escalado. Se ha desarrollado un nuevo Widget. Se toma la decisión de que todo el mundo utilice el nuevo y deje de utilizar el antiguo. Para motivar esto, los jefes de proyecto dicen "Eliminaremos el Widget antiguo el 15 de agosto; asegúrate de que te has convertido al nuevo Widget".

Este tipo de enfoque puede funcionar en un entorno de software pequeño, pero fracasa rápidamente a medida que aumenta tanto la profundidad como la amplitud del gráfico de dependencias. Los equipos dependen de un número cada vez mayor de Widgets, y una sola interrupción en la compilación puede afectar a un porcentaje cada vez mayor de la empresa. Resolver estos problemas de forma escalable significa cambiar la forma en que hacemos la depreciación: en lugar de empujar el trabajo de migración a los clientes, los equipos pueden internalizarlo ellos mismos, con todas las economías de escala que ello proporciona.

En 2012, intentamos poner fin a esto con reglas que mitigan el churn: los equipos de infraestructura deben hacer el trabajo de trasladar ellos mismos a sus usuarios internos a las nuevas versiones o hacer la actualización in situ, de forma compatible con versiones anteriores. Esta política, a la que hemos llamado "Regla del Churn", se adapta mejor: los proyectos dependientes ya no tienen que hacer un esfuerzo cada vez mayor para mantenerse al día. También hemos aprendido que hacer que un grupo dedicado de expertos ejecute el cambio escala mejor que pedir más esfuerzo de mantenimiento a cada usuario: los expertos dedican algún tiempo a aprender todo el problema en profundidad y luego aplican esa experiencia a cada subproblema. Obligar a los usuarios a responder a los cambios significa que cada equipo afectado hace un trabajo peor de puesta en marcha, resuelve su problema inmediato y luego desecha ese conocimiento ahora inútil. La experiencia se adapta mejor.

El uso tradicional de ramas de desarrollo es otro ejemplo de política que tiene problemas de escalado incorporados. Una organización puede identificar que la fusión de grandes características en el tronco ha desestabilizado el producto y concluir: "Necesitamos controles más estrictos sobre cuándo se fusionan las cosas. Deberíamos fusionar con menos frecuencia". Esto lleva rápidamente a que cada equipo o cada función tenga ramas de desarrollo separadas. Cada vez que se decide que una rama está "completa", se prueba y se fusiona con el tronco, lo que desencadena un trabajo potencialmente costoso para otros ingenieros que siguen trabajando en su rama de desarrollo, en forma de resincronización y pruebas. Esta gestión de ramas puede funcionar en una organización pequeña que tenga entre 5 y 10 ramas de este tipo. A medida que aumenta el tamaño de una organización (y el número de ramas), rápidamente se hace evidente que estamos pagando una cantidad cada vez mayor de gastos generales para hacer la misma tarea. Necesitaremos un enfoque diferente a medida que aumentemos de tamaño, y de ello hablaremos en el Capítulo 16.

Políticas que se adaptan bien

¿Qué tipo de políticas generan mejores costes a medida que crece la organización? O, mejor aún, ¿qué tipo de políticas podemos implantar que proporcionen un valor superlineal a medida que crece la organización?

Una de nuestras políticas internas favoritas es una gran ayuda para los equipos de infraestructura, ya que protege su capacidad para realizar cambios en la infraestructura de forma segura. "Si un producto experimenta interrupciones u otros problemas como consecuencia de cambios en la infraestructura, pero el problema no se detectó mediante pruebas en nuestro sistema de Integración Continua (IC), no es culpa del cambio de infraestructura". Más coloquialmente, esto se expresa como "Si te gustaba, deberías haberle puesto una prueba CI", lo que llamamos "La regla de Beyoncé".13 Desde el punto de vista del escalado, la Regla Beyoncé implica que las pruebas complicadas y puntuales a medida que no son activadas por nuestro sistema común de CI no cuentan. Sin ella, un ingeniero de un equipo de infraestructura podría tener que localizar a todos los equipos con código afectado y preguntarles cómo ejecutar sus pruebas. Podíamos hacerlo cuando había cien ingenieros. Pero ya no podemos permitírnoslo.

Hemos descubierto que la experiencia y los foros de comunicación compartidos ofrecen un gran valor a medida que una organización escala. A medida que los ingenieros debaten y responden preguntas en foros compartidos, el conocimiento tiende a extenderse. Los nuevos expertos crecen. Si tienes cien ingenieros escribiendo Java, un solo experto en Java amable y servicial, dispuesto a responder preguntas, pronto producirá cien ingenieros escribiendo mejor código Java. El conocimiento es viral, los expertos son portadores, y hay mucho que decir sobre el valor de eliminar los obstáculos comunes para tus ingenieros. Trataremos este tema con más detalle en el Capítulo 3.

Ejemplo: Actualización del compilador

Considera la desalentadora tarea de actualizar tu compilador. Teóricamente, la actualización de un compilador debería ser barata, dado el esfuerzo que requieren los lenguajes para ser compatibles con versiones anteriores, pero ¿hasta qué punto es una operación barata en la práctica? Si nunca has hecho una actualización de este tipo, ¿cómo evaluarías si tu código base es compatible con ese cambio?

Según nuestra experiencia, las actualizaciones de lenguajes y compiladores son tareas sutiles y difíciles, incluso cuando en general se espera que sean compatibles con versiones anteriores. Una actualización del compilador casi siempre dará lugar a cambios menores en el comportamiento: corregir errores de compilación, ajustar optimizaciones o cambiar potencialmente los resultados de cualquier cosa que antes fuera indefinida. ¿Cómo evaluarías la corrección de todo tu código frente a todos estos posibles resultados?

La actualización del compilador más famosa de la historia de Google tuvo lugar en 2006. En aquel momento, llevábamos unos cuantos años funcionando y teníamos varios miles de ingenieros en plantilla. No habíamos actualizado los compiladores en unos cinco años. La mayoría de nuestros ingenieros no tenían experiencia con un cambio de compilador. La mayor parte de nuestro código había estado expuesto a una sola versión del compilador. Fue una tarea difícil y penosa para un equipo de voluntarios (en su mayoría), que acabó convirtiéndose en una cuestión de encontrar atajos y simplificaciones para sortear los cambios de compilador y lenguaje que no sabíamos cómo adoptar.14 Al final, la actualización del compilador de 2006 fue extremadamente dolorosa. Muchos problemas de la Ley de Hyrum, grandes y pequeños, se habían colado en el código base y habían servido para aumentar nuestra dependencia de una versión concreta del compilador. Romper esas dependencias implícitas era doloroso. Los ingenieros en cuestión se arriesgaban: aún no teníamos la Regla de Beyoncé, ni un sistema de CI omnipresente, por lo que era difícil conocer el impacto del cambio con antelación o estar seguros de que no se les culparía de las regresiones.

Esta historia no es en absoluto inusual. Los ingenieros de muchas empresas pueden contar una historia similar sobre una actualización dolorosa. Lo inusual es que reconocimos a posteriori que la tarea había sido dolorosa y empezamos a centrarnos en cambios tecnológicos y organizativos para superar los problemas de escala y convertir la escala en una ventaja: automatización (para que un solo humano pueda hacer más), consolidación/consistencia (para que los cambios de bajo nivel tengan un alcance limitado del problema) y experiencia (para que unos pocos humanos puedan hacer más).

Cuanto más frecuentemente cambies tu infraestructura, más fácil te resultará hacerlo. Hemos comprobado que la mayoría de las veces, cuando el código se actualiza como parte de algo como una actualización del compilador, se vuelve menos quebradizo y más fácil de actualizar en el futuro. En un ecosistema en el que la mayor parte del código ha pasado por varias actualizaciones, deja de depender de los matices de la implementación subyacente; en su lugar, depende de la abstracción real garantizada por el lenguaje o el SO. Independientemente de lo que estés actualizando exactamente, espera que la primera actualización de una base de código sea significativamente más cara que las actualizaciones posteriores, incluso controlando otros factores.

A través de ésta y otras experiencias, hemos descubierto muchos factores que afectan a la flexibilidad de un código base:

Experiencia
Sabemos cómo hacerlo; para algunos lenguajes, ya hemos realizado cientos de actualizaciones de compiladores en muchas plataformas.
Estabilidad
Hay menos cambios entre versiones porque adoptamos versiones con más regularidad; para algunos idiomas, ahora desplegamos actualizaciones del compilador cada una o dos semanas.
Conformidad
Hay menos código que no haya pasado ya por una actualización, de nuevo porque actualizamos regularmente.
Familiaridad
Como hacemos esto con suficiente regularidad, podemos detectar redundancias en el proceso de realizar una actualización e intentar automatizarlo. Esto se solapa significativamente con los puntos de vista de la SRE sobre el trabajo.15
Política
Tenemos procesos y políticas como la Regla Beyoncé. El efecto neto de estos procesos es que las actualizaciones siguen siendo factibles porque los equipos de infraestructura no tienen que preocuparse de cada uso desconocido, sólo de los que son visibles en nuestros sistemas CI .

La lección subyacente no es sobre la frecuencia o la dificultad de las actualizaciones del compilador, sino que, en cuanto fuimos conscientes de que las tareas de actualización del compilador eran necesarias, encontramos formas de asegurarnos de realizarlas con un número constante de ingenieros, incluso a medida que crecía la base de código.16 Si en lugar de eso hubiéramos decidido que la tarea era demasiado costosa y debía evitarse en el futuro, podríamos seguir utilizando una versión del compilador de hace una década. Estaríamos pagando quizás un 25% más por recursos informáticos como resultado de las oportunidades de optimización perdidas. Nuestra infraestructura central podría ser vulnerable a importantes riesgos de seguridad, dado que un compilador de la era de 2006 no está ayudando ciertamente a mitigar las vulnerabilidades de la ejecución especulativa. El estancamiento es una opción, pero a menudo no es acertada.

Cambiar a la izquierda

Una de las grandes verdades que hemos visto que es cierta es la idea de que encontrar los problemas antes en el flujo de trabajo del desarrollador suele reducir los costes. Considera una línea de tiempo del flujo de trabajo del desarrollador para una función que progresa de izquierda a derecha, empezando por la concepción y el diseño, pasando por la implementación, la revisión, las pruebas, el commit, el canary y, finalmente, la implementación en producción. Desplazar la detección de problemas a la "izquierda" en una fase más temprana de esta línea temporal hace que sea más barato solucionarlos que esperar más tiempo, como se muestra en la Figura 1-2.

Este término parece tener su origen en los argumentos de que la seguridad no debe aplazarse hasta el final del proceso de desarrollo, con los consiguientes llamamientos a "cambiar a la izquierda en materia de seguridad". El argumento en este caso es relativamente sencillo: si un problema de seguridad se descubre sólo después de que tu producto haya pasado a producción, tienes un problema muy caro. Si se detecta antes de la implementación en producción, aún puede costar mucho trabajo identificar y solucionar el problema, pero es más barato. Si puedes detectarlo antes de que el desarrollador original confirme el fallo en el control de versiones, es aún más barato: ya conoce la función; revisarla de acuerdo con las nuevas restricciones de seguridad es más barato que confirmarla y obligar a otra persona a triarla y arreglarla.

Timeline of the developer workflow
Figura 1-2. Cronología del flujo de trabajo del desarrollador

El mismo patrón básico aparece muchas veces en este libro. Los errores que se detectan mediante el análisis estático y la revisión del código antes de ser confirmados son mucho más baratos que los errores que llegan a producción. Proporcionar herramientas y prácticas que pongan de relieve la calidad, la fiabilidad y la seguridad en una fase temprana del proceso de desarrollo es un objetivo común para muchos de nuestros equipos de infraestructura. No es necesario que ningún proceso o herramienta sea perfecto, por lo que podemos adoptar un enfoque de defensa en profundidad, con la esperanza de detectar el mayor número posible de defectos en el lado izquierdo del gráfico.

Contrapartidas y costes

Si entendemos cómo programar, entendemos la vida útil del software que mantenemos y entendemos cómo mantenerlo a medida que escalamos con más ingenieros produciendo y manteniendo nuevas características, todo lo que queda es tomar buenas decisiones. Esto parece obvio: en la ingeniería de software, como en la vida, las buenas decisiones conducen a buenos resultados. Sin embargo, las ramificaciones de esta observación se pasan por alto fácilmente. Dentro de Google, hay una gran aversión al "porque lo digo yo". Es importante que haya una persona que decida sobre cualquier tema y vías claras de escalada cuando las decisiones parezcan equivocadas, pero el objetivo es el consenso, no la unanimidad. Está bien y es de esperar ver algunos casos de "no estoy de acuerdo con tu métrica/valoración, pero entiendo cómo puedes llegar a esa conclusión". Inherente a todo esto está la idea de que tiene que haber una razón para todo; "porque sí", "porque lo digo yo" o "porque todo el mundo lo hace así" son lugares donde acechan las malas decisiones. Siempre que sea eficiente hacerlo, deberíamos poder explicar nuestro trabajo al decidir entre los costes generales de dos opciones de ingeniería.

¿Qué entendemos por coste? No estamos hablando sólo de dólares. "Coste" se traduce aproximadamente por esfuerzo y puede implicar alguno o todos estos factores:

  • Costes financieros (por ejemplo, dinero)

  • Costes de recursos (por ejemplo, tiempo de CPU)

  • Costes de personal (por ejemplo, esfuerzo de ingeniería)

  • Costes de transacción (por ejemplo, ¿cuánto cuesta actuar?)

  • Costes de oportunidad (por ejemplo, ¿cuánto cuesta no actuar?)

  • Costes sociales (por ejemplo, ¿qué impacto tendrá esta elección en la sociedad en general?)

Históricamente, ha sido especialmente fácil ignorar la cuestión de los costes sociales. Sin embargo, Google y otras grandes empresas tecnológicas pueden ahora implementar de forma creíble productos con miles de millones de usuarios. En muchos casos, estos productos son un claro beneficio neto, pero cuando operamos a tal escala, incluso las pequeñas discrepancias en cuanto a usabilidad, accesibilidad, equidad o potencial de abuso se magnifican, a menudo en detrimento de grupos que ya están marginados. El software impregna tantos aspectos de la sociedad y la cultura; por tanto, es prudente que seamos conscientes tanto de lo bueno como de lo malo que permitimos al tomar decisiones técnicas y sobre productos. Hablaremos mucho más de esto en el Capítulo 4.

Además de los costes mencionados (o de nuestra estimación de los mismos), existen sesgos: el sesgo del statu quo, la aversión a las pérdidas y otros. Cuando evaluamos el coste, tenemos que tener en cuenta todos los costes enumerados anteriormente: la salud de una organización no es sólo si hay dinero en el banco, sino también si sus miembros se sienten valorados y productivos. En campos altamente creativos y lucrativos como la ingeniería de software, el coste financiero no suele ser el factor limitante, sino el coste de personal. Los aumentos de eficiencia derivados de mantener a los ingenieros contentos, concentrados y comprometidos pueden dominar fácilmente otros factores, simplemente porque la concentración y la productividad son muy variables, y es fácil imaginar una diferencia del 10 al 20%.

Ejemplo: Marcadores

En muchas organizaciones, los rotuladores de pizarra blanca se tratan como bienes preciosos. Están estrictamente controlados y siempre escasean. Invariablemente, la mitad de los rotuladores de cualquier pizarra blanca están secos e inservibles. ¿Cuántas veces has estado en una reunión que se ha visto interrumpida por la falta de un rotulador que funcionara? ¿Cuántas veces has perdido el hilo de tus pensamientos porque se te ha acabado un rotulador? ¿Cuántas veces han desaparecido todos los rotuladores, presumiblemente porque algún otro equipo se quedó sin rotuladores y tuvo que huir con los tuyos? Y todo por un producto que cuesta menos de un dólar.

Google suele tener armarios abiertos llenos de material de oficina, incluidos rotuladores de pizarra, en la mayoría de las áreas de trabajo. En un momento es fácil coger docenas de rotuladores de varios colores. En algún momento hicimos un compromiso explícito: es mucho más importante optimizar la lluvia de ideas sin obstáculos que protegerse de que alguien se vaya con un montón de rotuladores.

Nuestro objetivo es tener los ojos abiertos y sopesar explícitamente los costes y beneficios de todo lo que hacemos, desde el material de oficina y las ventajas de los empleados, pasando por la experiencia diaria de los desarrolladores, hasta la forma de suministrar y ejecutar servicios a escala mundial. A menudo decimos: "Google es una cultura basada en los datos". En realidad, es una simplificación: incluso cuando no hay datos, puede haber pruebas, precedentes y argumentos. Tomar buenas decisiones de ingeniería consiste en sopesar todos los datos disponibles y tomar decisiones informadas sobre las compensaciones. A veces, esas decisiones se basan en el instinto o en las buenas prácticas aceptadas, pero sólo después de haber agotado los enfoques que intentan medir o estimar los verdaderos costes subyacentes.

Al final, las decisiones en un grupo de ingeniería deberían reducirse a muy pocas cosas:

  • Lo hacemos porque debemos hacerlo (requisitos legales, requisitos del cliente).

  • Lo hacemos porque es la mejor opción (según determine algún decisor apropiado) que podemos ver en ese momento, basándonos en las pruebas actuales.

Las decisiones no deben ser "Hacemos esto porque yo lo digo".17

Aportaciones a la toma de decisiones

Cuando estamos sopesando datos, nos encontramos con dos escenarios comunes:

  • Todas las cantidades implicadas son medibles o, al menos, pueden estimarse. Esto suele significar que estamos evaluando compensaciones entre CPU y red, o entre dólares y RAM, o considerando si gastar dos semanas de tiempo de ingeniería para ahorrar N CPUs en nuestros centros de datos.

  • Algunas de las cantidades son sutiles, o no sabemos cómo medirlas. A veces esto se manifiesta como "No sabemos cuánto tiempo de ingeniería llevará esto". A veces es incluso más nebuloso: ¿cómo se mide el coste de ingeniería de una API mal diseñada? ¿O el impacto social de la elección de un producto?

Hay pocas razones para ser deficiente en el primer tipo de decisión. Cualquier organización de ingeniería de software puede y debe hacer un seguimiento del coste actual de los recursos informáticos, las horas de ingeniero y otras cantidades con las que interactúa regularmente. Aunque no quieras dar a conocer a tu organización las cantidades exactas en dólares, puedes elaborar una tabla de conversión: tantas CPU cuestan lo mismo que tanta RAM o tanto ancho de banda de red.

Con una tabla de conversión acordada en la mano, cada ingeniero puede hacer su propio análisis. "Si dedico dos semanas a cambiar esta lista enlazada por una estructura de mayor rendimiento, voy a utilizar cinco gibibytes más de RAM de producción, pero ahorraré dos mil CPU. ¿Debo hacerlo?" Esta pregunta no sólo depende del coste relativo de la RAM y las CPU, sino también de los costes de personal (dos semanas de apoyo para un ingeniero de software) y de los costes de oportunidad (¿qué más podría producir ese ingeniero en dos semanas?).

Para el segundo tipo de decisión, no hay una respuesta fácil. Confiamos en la experiencia, el liderazgo y los precedentes para negociar estas cuestiones. Estamos invirtiendo en investigación para que nos ayude a cuantificar lo difícil de cuantificar (véase el Capítulo 7). Sin embargo, la mejor sugerencia general que tenemos es ser conscientes de que no todo es mensurable o predecible e intentar tratar esas decisiones con la misma prioridad y mayor cuidado. A menudo son igual de importantes, pero más difíciles de gestionar.

Ejemplo: Construcciones distribuidas

Piensa en tu compilación. Según una encuesta de Twitter totalmente acientífica, entre el 60 y el 70% de los desarrolladores construyen localmente, incluso con las grandes y complicadas compilaciones de hoy en día. Esto nos lleva directamente a las no-bromas, como ilustra este cómic "Compilando":¿cuánto tiempo productivo de tu organización se pierde esperando una compilación? Compáralo con el coste de ejecutar algo como distcc para un grupo pequeño. O, ¿cuánto cuesta gestionar una pequeña granja de compilación para un grupo grande? ¿Cuántas semanas/meses hacen falta para que esos costes supongan una ganancia neta?

A mediados de la década de 2000, Google se basaba exclusivamente en un sistema de compilación local: sacabas el código y lo compilabas localmente. En algunos casos teníamos máquinas locales enormes (¡podías compilar Maps en tu escritorio!), pero los tiempos de compilación se hacían cada vez más largos a medida que crecía el código base. Como era de esperar, incurrimos en crecientes gastos generales de personal debido al tiempo perdido, así como en mayores costes de recursos para máquinas locales más grandes y potentes, etc. Estos costes de recursos eran especialmente problemáticos: por supuesto, queremos que la gente tenga una compilación lo más rápida posible, pero la mayor parte del tiempo, una máquina de desarrollo de escritorio de alto rendimiento estará inactiva. No parece la forma adecuada de invertir esos recursos.

Finalmente, Google desarrolló su propio sistema de compilación distribuido. El desarrollo de este sistema tuvo un coste, por supuesto: llevó tiempo a los ingenieros desarrollarlo, llevó más tiempo a los ingenieros cambiar los hábitos y el flujo de trabajo de todos y aprender el nuevo sistema y, por supuesto, costó recursos informáticos adicionales. Pero el ahorro global mereció claramente la pena: las construcciones se hicieron más rápidas, se recuperó el tiempo de los ingenieros y la inversión en hardware pudo centrarse en la infraestructura compartida gestionada (en realidad, un subconjunto de nuestra flota de producción) en lugar de en máquinas de sobremesa cada vez más potentes. El Capítulo 18 entra en más detalles sobre nuestro enfoque de las compilaciones distribuidas y las compensaciones pertinentes.

Así que construimos un nuevo sistema, lo implementamos en producción y aceleramos la construcción de todo el mundo. ¿Es ése el final feliz de la historia? No del todo: proporcionar un sistema de compilación distribuido mejoró enormemente la productividad de los ingenieros, pero con el tiempo, las propias compilaciones distribuidas se hincharon. Lo que en el caso anterior estaba limitado por los ingenieros individuales (porque tenían un interés personal en mantener sus compilaciones locales lo más rápidas posible), ahora no lo estaba en un sistema de compilación distribuido. Las dependencias hinchadas o innecesarias en el gráfico de construcción se hicieron demasiado comunes. Cuando todos sentían directamente el dolor de una compilación no óptima y se les incentivaba a estar atentos, los incentivos estaban mejor alineados. Al eliminar esos incentivos y ocultar las dependencias hinchadas en una construcción distribuida paralela, creamos una situación en la que el consumo podía desbocarse, y casi nadie estaba incentivado para vigilar la hinchazón de la construcción. Esto recuerda a la paradoja de Jevons: el consumo de un recurso puede aumentar como respuesta a una mayor eficiencia en su uso.

En conjunto, los costes ahorrados asociados a la incorporación de un sistema de construcción distribuida superaron con creces los costes negativos asociados a su construcción y mantenimiento. Pero, como vimos con el aumento del consumo, no previmos todos estos costes. Habiéndonos adelantado, nos encontramos en una situación en la que necesitábamos reconceptualizar los objetivos y las limitaciones del sistema y de nuestro uso, identificar las buenas prácticas (pequeñas dependencias, gestión de dependencias por máquinas) y financiar las herramientas y el mantenimiento del nuevo ecosistema. Incluso una compensación relativamente sencilla del tipo "Gastaremos $$$s en recursos informáticos para recuperar el tiempo de los ingenieros" tuvo efectos posteriores imprevistos.

Ejemplo: Decidir entre tiempo y escala

La mayor parte del tiempo, nuestros temas principales de tiempo y escala se solapan y funcionan conjuntamente. Una política como la Regla Beyoncé escala bien y nos ayuda a mantener las cosas a lo largo del tiempo. Un cambio en la interfaz de un SO puede requerir muchas pequeñas refactorizaciones para adaptarse a él, pero la mayoría de esos cambios se escalarán bien porque tienen una forma similar: el cambio del SO no se manifiesta de forma diferente para cada persona que llama y cada proyecto.

De vez en cuando, el tiempo y la escala entran en conflicto, y en ningún sitio tan claramente como en la pregunta básica: ¿deberíamos añadir una dependencia o bifurcarla/reimplementarla para que se adapte mejor a nuestras necesidades locales?

Esta cuestión puede plantearse en muchos niveles de la pila de software, porque suele ocurrir que una solución a medida personalizada para tu estrecho espacio de problemas puede superar a la solución de utilidad general que debe manejar todas las posibilidades. Al bifurcar o reimplementar el código de utilidad y personalizarlo para tu estrecho dominio, puedes añadir nuevas funciones con mayor facilidad, u optimizar con mayor certeza, independientemente de si hablamos de un microservicio, una caché en memoria, una rutina de compresión o cualquier otra cosa de nuestro ecosistema de software. Quizá lo más importante sea que el control que obtienes de una bifurcación de este tipo te aísla de los cambios en tus dependencias subyacentes: esos cambios no los dicta otro equipo o un proveedor externo. Tú tienes el control de cómo y cuándo reaccionar ante el paso del tiempo y la necesidad de cambiar.

Por otra parte, si cada desarrollador bifurca todo lo que utiliza en su proyecto de software en lugar de reutilizar lo que existe, la escalabilidad sufre junto con la sostenibilidad. Reaccionar ante un problema de seguridad en una biblioteca subyacente ya no es cuestión de actualizar una única dependencia y a sus usuarios: ahora es cuestión de identificar cada bifurcación vulnerable de esa dependencia y a los usuarios de esas bifurcaciones.

Como ocurre con la mayoría de las decisiones de ingeniería de software, no hay una respuesta única para esta situación. Si la vida de tu proyecto es corta, las bifurcaciones son menos arriesgadas. Si la bifurcación en cuestión tiene un alcance limitado demostrable, también ayuda evitar las bifurcaciones de interfaces que podrían funcionar más allá de los límites temporales o de tiempo del proyecto (estructuras de datos, formatos de serialización, protocolos de red). La coherencia tiene un gran valor, pero la generalidad conlleva sus propios costes, y a menudo puedes ganar haciendo lo tuyo, si lo haces con cuidado.

Revisar decisiones, cometer errores

Uno de los beneficios no reconocidos de de comprometerse con una cultura basada en los datos es la capacidad y la necesidad combinadas de admitir los errores. En algún momento se tomará una decisión, basada en los datos disponibles -con suerte, basada en buenos datos y sólo unos pocos supuestos, pero implícitamente basada en los datos disponibles en ese momento-. A medida que aparecen nuevos datos, cambian los contextos o se disipan las suposiciones, puede quedar claro que una decisión era errónea o que tenía sentido en su momento pero ya no lo tiene. Esto es especialmente crítico para una organización longeva: el tiempo no sólo provoca cambios en las dependencias técnicas y los sistemas de software, sino también en los datos utilizados para tomar decisiones.

Creemos firmemente en que los datos informan las decisiones, pero reconocemos que los datos cambiarán con el tiempo, y que pueden presentarse nuevos datos. Esto significa, inherentemente, que habrá que revisar las decisiones de vez en cuando a lo largo de la vida del sistema en cuestión. Para los proyectos de larga duración, a menudo es fundamental tener la capacidad de cambiar de dirección una vez tomada una decisión inicial. Y, lo que es más importante, significa que los que deciden deben tener derecho a admitir errores. En contra del instinto de algunas personas, los líderes que admiten errores son más respetados, no menos.

Guíate por las pruebas, pero también date cuenta de que las cosas que no se pueden medir pueden seguir teniendo valor. Si eres un líder, eso es lo que se te ha pedido que hagas: ejercer tu criterio, afirmar que las cosas son importantes. Hablaremos más sobre el liderazgo en los Capítulos 5 y 6.

Ingeniería de software frente a programación

Cuando se te presenta con nuestra distinción entre ingeniería de software y programación, podrías preguntarte si hay un juicio de valor inherente en juego. ¿Es la programación algo peor que la ingeniería de software? ¿Es un proyecto que se espera que dure una década con un equipo de cientos de personas intrínsecamente más valioso que uno que sólo es útil durante un mes y construido por dos personas?

Por supuesto que no. Lo que queremos decir no es que la ingeniería de software sea superior, sino que se trata de dos ámbitos problemáticos diferentes con limitaciones, valores y buenas prácticas distintos. Más bien, el valor de señalar esta diferencia viene de reconocer que algunas herramientas son estupendas en un dominio, pero no en el otro. Probablemente no necesites confiar en las pruebas de integración (véase el Capítulo 14) y en las prácticas de Implementación Continua (CD) (véase el Capítulo 24) para un proyecto que sólo durará unos días. Del mismo modo, todas nuestras preocupaciones a largo plazo sobre el versionado semántico (SemVer) y la gestión de dependencias en los proyectos de ingeniería de software (véase el Capítulo 21) no se aplican realmente a los proyectos de programación a corto plazo: utiliza lo que esté disponible para resolver la tarea que tengas entre manos.

Creemos que es importante diferenciar entre los términos relacionados pero distintos de "programación" e "ingeniería de software". Gran parte de esa diferencia se debe a la gestión del código a lo largo del tiempo, al impacto del tiempo en la escala y a la toma de decisiones frente a esas ideas. La programación es el acto inmediato de producir código. La ingeniería de software es el conjunto de políticas, prácticas y herramientas necesarias para que ese código sea útil durante todo el tiempo que sea necesario utilizarlo y permita la colaboración entre un equipo.

Conclusión

Este libro trata todos estos temas: políticas para una organización y para un solo programador, cómo evaluar y perfeccionar tus buenas prácticas, y las herramientas y tecnologías que intervienen en un software mantenible. Google ha trabajado duro para tener una base de código y una cultura sostenibles. No creemos necesariamente que nuestro enfoque sea la única y verdadera forma de hacer las cosas, pero sí proporciona una prueba mediante el ejemplo de que se puede hacer. Esperamos que proporcione un marco útil para reflexionar sobre el problema general: ¿cómo mantener tu código durante el tiempo necesario para que siga funcionando?

TL;DRs

  • La "ingeniería de software" difiere de la "programación" en la dimensionalidad: la programación consiste en producir código. La ingeniería de software lo amplía para incluir el mantenimiento de ese código durante su vida útil.

  • Hay un factor de al menos 100.000 veces entre la vida útil del código de corta duración y la del código de larga duración. Es absurdo suponer que las mismas buenas prácticas se aplican universalmente en ambos extremos de ese espectro.

  • El software es sostenible cuando, durante la vida útil prevista del código, somos capaces de responder a los cambios en las dependencias, la tecnología o los requisitos del producto. Podemos optar por no cambiar las cosas, pero tenemos que ser capaces.

  • Ley de Hyrum: con un número suficiente de usuarios de una API, no importa lo que prometas en el contrato: todos los comportamientos observables de tu sistema dependerán de alguien.

  • Cada tarea que tu organización tenga que hacer repetidamente debe ser escalable (lineal o mejor) en términos de aportación humana. Las políticas son una herramienta maravillosa para hacer que los procesos sean escalables.

  • Las ineficiencias de los procesos y otras tareas de desarrollo de software tienden a escalar lentamente. Ten cuidado con los problemas de rana hervida.

  • La experiencia es especialmente rentable cuando se combina con economías de escala.

  • "Porque lo digo yo" es una razón terrible para hacer las cosas.

  • Guiarse por los datos es un buen comienzo, pero en realidad, la mayoría de las decisiones se basan en una mezcla de datos, suposiciones, precedentes y argumentos. Lo mejor es que los datos objetivos constituyan la mayor parte de esas aportaciones, pero rara vez pueden ser todas.

  • Guiarse por los datos a lo largo del tiempo implica la necesidad de cambiar de dirección cuando los datos cambian (o cuando se disipan las suposiciones). Los errores o la revisión de los planes son inevitables.

1 No nos referimos a la "vida útil de ejecución", sino a la "vida útil de mantenimiento": ¿cuánto tiempo se seguirá construyendo, ejecutando y manteniendo el código? ¿Durante cuánto tiempo proporcionará valor este software?

2 Esta es quizás una definición razonable de la deuda técnica: cosas que "deberían" estar hechas, pero que aún no lo están: el delta entre nuestro código y lo que desearíamos que fuera.

3 Considera también la cuestión de si sabemos de antemano que un proyecto va a ser de larga duración.

4 Existen dudas sobre la atribución original de esta cita; el consenso parece ser que fue redactada originalmente por Brian Randell o Margaret Hamilton, pero podría haber sido inventada en su totalidad por Dave Parnas. La cita habitual es "Técnicas de ingeniería de software": Informe de una conferencia patrocinada por el Comité Científico de la OTAN", Roma, Italia, 27-31 de octubre de 1969, Bruselas, División de Asuntos Científicos, OTAN.

5 Frederick P. Brooks Jr. The Mythical Man-Month: Ensayos sobre ingeniería de software (Boston: Addison-Wesley, 1995).

6 Appcelerator, "Nothing is Certain Excepcept Death, Taxes and a Short Mobile App Lifespan", blog Axway Developer, 6 de diciembre de 2012.

7 Tus propias prioridades y gustos determinarán dónde se produce exactamente esa transición. Hemos descubierto que la mayoría de los proyectos parecen dispuestos a actualizarse en un plazo de cinco años. Entre 5 y 10 años parece una estimación conservadora para esta transición en general.

8 En su honor, Hyrum se esforzó mucho por llamar humildemente a esto "La Ley de las Dependencias Implícitas", pero "Ley de Hyrum" es la abreviatura con la que se ha quedado la mayoría de la gente de Google.

9 Véase "Workflow", un cómic de xkcd.

10 Tipo de ataque de denegación de servicio (DoS) en el que un usuario no fiable conoce la estructura de una tabla hash y la función hash y proporciona datos de tal forma que se degrada el rendimiento algorítmico de las operaciones sobre la tabla.

11 Beyer, B. et al. Ingeniería de Fiabilidad del Sitio: Cómo ejecuta Google los sistemas de producción. (Boston: O'Reilly Media, 2016).

12 Siempre que utilicemos "escalable" en un contexto informal en este capítulo, nos referiremos a "escalado sublineal con respecto a las interacciones humanas".

13 Es una referencia a la popular canción "Single Ladies", que incluye el estribillo "If you liked it then you shoulda put a ring on it".

14 En concreto, era necesario hacer referencia a las interfaces de la biblioteca estándar de C++ en el espacio de nombres std, y un cambio de optimización para std::string resultó ser una pesimización significativa para nuestro uso, por lo que fueron necesarias algunas soluciones adicionales.

15 Beyer et al. Ingeniería de la Fiabilidad del Sitio Web: How Google Runs Production Systems, capítulo 5, "Eliminating Toil".

16 Según nuestra experiencia, un ingeniero de software (SWE) medio produce un número bastante constante de líneas de código por unidad de tiempo. Para una población fija de SWE, una base de código crece linealmente proporcional al recuento de SWE-meses a lo largo del tiempo. Si tus tareas requieren un esfuerzo que escala con las líneas de código, eso es preocupante.

17 Esto no quiere decir que las decisiones deban tomarse por unanimidad, ni siquiera con un amplio consenso; al final, alguien debe decidir. Se trata principalmente de una declaración sobre cómo debe fluir el proceso de toma de decisiones para quien sea realmente responsable de la decisión.

Get Ingeniería de software en Google now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.