¿Realmente es difícil hacer software? (I)

En esta serie de dos artículos intentaré explicar cuál es el origen de la inmensidad de problemas que tiene una disciplina –en apariencia sencilla– como es el desarrollo de software. La serie va dirigida a todos mis colegas, para que se armen de argumentos cuando tengan que explicar a qué nos enfrentamos, y a otros profesionales que deseen entender qué pasa en este mundo; muy especialmente, a aquellos miembros de la alta dirección de las empresas tecnológicas que sigan sin entender por qué se retraso ese proyecto o por qué la entrega de aquel otro terminó en desastre. Les adelanto que no se debió a la incapacidad de los ingenieros.

El software es un sistema complejo

Una aplicación creada por una persona en unos pocos meses no suele tener una gran complejidad, pero este no es el tipo de software que encargan los clientes a las empresas para las que trabajamos. Lo normal es que un proyecto necesite la colaboración –casi un engranaje perfecto– de un grupo mayor o menos de personas y sean necesarios seis, doce o más meses para poder terminarlo. El sistema resultante encerrará una gran complejidad. Sin embargo, lo que para unos es muy complejo para otros podría serlo en menor grado. Para no relativizar el término es conveniente saber qué es un sistema complejo. Booch [1], basado en el trabajo de Courtois [4], Simon [11], Rechtin [9] y Gall [6] concluye que todo sistema complejo tiene cinco atributos característicos.

Es jerárquico

Un sistema complejo siempre está compuesto de subsistemas que colaboran entre sí, los cuales contiene otros subsistemas y así sucesivamente.

El universo esta formado por cúmulos conformados por galaxias, las cuales contienen gases, asteroides y sistemas estelares compuestos por una o dos estrellas y sistemas planetarios configurados por un planeta y sus satélites.

En el caso del software, la jerarquía empieza en los procesos –programas en ejecución– creados mediante la combinación de componentes, los cuales contienen paquetes que almacenan clases constituidas por datos y funciones. Fuera del paradigma orientado a objetos, sustitúyase clases por archivos.

Si los sistemas complejos no fueran jerárquicos, entenderlos sería casi imposible. La comprensión de un sistema informático compuesto, simplemente, por miles de datos y decenas de miles de funciones raya lo utópico.

Sus elementos constitutivos son primitivos

Cada uno de los núcleos de un microprocesador está compuesto por diversos elementos como las unidades de control, las unidades aritmeticológicas, las unidades de cálculo en coma flotante, los bancos de registros, las memorias caché, etc. Sin embargo, por muy diferente que sea el cometido de cada una de las partes, todas ellas se construyen a partir de un elemento primitivo: el transistor.

Los elementos primitivos del software son los datos más simples caracterizados por los tipos primitivos. Todos estos tipos, al fin y a la postre, son numéricos porque, aunque el lenguaje incorpore en la categoría de primitivos tipos como la cadena de caracteres o la fecha, estos no dejan de ser números. Incluso en la orientación a objetos, donde hay jerarquías de composición en las cuales el todo –el objeto principal– está formado por la suma de las partes –objetos con los que está asociado directamente o indirectamente– es fácil observar que en los objetos terminales de la jerarquía habitualmente solo hay tipos primitivos. Si un objeto terminal almacena un tipo no primitivo será porque está apoyándose en un componente que se lo proporciona. Al inspeccionar dicho componente, terminaríamos por ver que ese tipo complejo también está formado por tipos primitivos.

Existe separación de intereses entre las partes

Los enlaces internos dentro de los componentes suelen ser mucho más fuertes que los enlaces entre los componentes. Esta diferencia proporciona una separación clara de intereses entres las distintas partes del sistema y facilita su estudio de forma más o menos aislada.

Resulta obvio que la relación entre el disco duro de un ordenador y el microprocesador es más débil que aquella existente entre los elementos que los forman. Es perfectamente posible estudiar cómo funciona un microprocesador dejando de lado los detalles de funcionamiento del disco.

En lo relativo al software, los enlaces internos de los paquetes –las relaciones entre las clases que los forman– son mucho más fuertes que las relaciones entre los propios paquetes. A otro nivel, puede establecerse una conclusión similar respecto a los procesos y los componentes como las bibliotecas.

Tiene patrones comunes

Es raro encontrar en un sistema complejo cientos de subsistemas diferentes. Lo más normal es que tengan solo unas pocas clases de subsistemas en diferentes configuraciones.

Las puertas lógicas electrónicas, uno de los subsistemas de un ordenador, no son heterogéneas, no hay cien tipos diferentes de puertas lógicas. Este hecho permite su reutilización y poder encontrarlas, en diferentes configuraciones, tanto en la memoria principal, el microprocesador o el disco duro.

En el software son los objetos, con su patrón común de cómo se envían mensajes unos a otros, lo que permite su reutilización. En el caso de la programación estructura es similar solo que es en los archivos contenedores de funciones y datos donde unas funciones invocan a otras.

Tiene forma intermedias estables

Los anfibios evolucionaron de los peces porque estos eran formas biológicas estables. Si hubiera habido algún tipo de problema, la evolución hacia los anfibios habría sido más lenta o, quizás, jamás se habría producido.

Esta característica de los sistemas complejos aplicada al software da lugar a la ley de Gall, enunciada por primera vez en 1977 en la primera edición de [6]:

«Todo sistema complejo que funciona ha evolucionado a partir de uno más simple que funcionaba».

Por tanto, intentar crear todo un sistema complejo desde cero y de una sola vez conduce al fracaso. Sin embargo, si dicho sistema se va creando a partir de otro mucho más simple que se hace evolucionar, las probabilidades de éxito se disparan. En otras palabras: los sistemas deben construirse de forma incremental.

Además, a medida que los sistemas evolucionan, aquellos que una vez fueron complejos –el desarrollo de una biblioteca, por ejemplo– se convierten en objetos más simples sobre los que construir sistemas más complejos aún, como una aplicación que utiliza veinte bibliotecas.

Tras exponer cómo el software cumple con los cinco atributos de un sistema complejo y, en consecuencia, lo es; a continuación veremos cuáles son las características propias de su complejidad.

La complejidad inherente al software

Hay seis características que hacen al software tan especial desde el punto de vista de su complejidad: el tamaño, los dominios, la variabilidad, la invisibilidad, su gestión y que es un sistema discreto.

La cuestión cuantitativa

El software es una de las cosas creadas por el ser humano que más compleja es en relación a su tamaño. Cuanto más grande es, más complejo se torna. El pilar fundamental que sustenta esta afirmación es que, al contrario que en la electrónica, la mecánica o la construcción –donde abundan los elementos repetidos– en el software no hay dos partes iguales. De haberlas, se combinan para dejar solo una.

Cuando en 2001 se lanzó el primer microprocesador de dos núcleos –el POWER4® de IBM– se esperaba que la microelectrónica fuera capaz de crear microprocesadores de cuatro, seis, ocho o diez núcleos en un periodo relativamente breve de tiempo, como así ha sido. No quiero que se me mal intérprete con este ejemplo, la integración de más y más núcleos implica resolver complejı́simos problemas de diseño pero, se mire por donde se mire, un microprocesador de diez núcleos tiene una parte que se repite diez veces.

Una aplicación que pase de 25 000 a 50 000 líneas de código no doblará su complejidad –será mayor– porque no tendrá dos partes iguales. La complejidad del software en relación a su tamaño no crece de forma lineal sino, más bien, de forma exponencial. Siendo así, no sorprenden los resultados del informe CHAOS [7] donde solo el 6 % de los proyectos de gran tamaño acaban siendo un éxito frente a un 61 % de los pequeños proyectos.

El software también muestra cifras notables al analizar su tamaño en término absolutos. El número de líneas de código suele ser la métrica habitual para determinar el tamaño del software. No obstante, la complejidad de una aplicación pequeña puede llegar a ser superior a la de otra más grande porque existen otros factores condicionantes. El tamaño de una aplicación podría clasificarse como:

  • Muy pequeña. Aquella que tiene miles de líneas de código.
  • Pequeña. Decenas de miles.
  • Mediana. Cientos de miles.
  • Grande. Millones.
  • Enorme. Decenas de millones.

A veces no somos conscientes de qué implica escribir decenas de miles de líneas de código. El Quijote tiene alrededor de 381 000 palabras; si tomamos que hay doce palabras por línea –obviamente esto es muy relativo porque depende de la edición, pero parece un número razonable– obtendríamos un total de 31 750 líneas. Ahora, seleccione a un grupo de «escritores de programas» con un guion impuesto, que no pueden inventarse, y cuyo contenido –alrededor del 25%– va a ir cambiando y dígales que se pongan de acuerdo para escribir un «libro» con 31 750 líneas en seis meses donde todo tenga sentido. ¿Misión imposible?

El dominio ajeno

Si hay algo que distingue al ingeniero de software de otros ingenieros es el dominio que informatiza el sistema a desarrollar. Aparte del conocimiento necesario para ejercer nuestra profesión, necesitamos comprender el ámbito del que se ocupa el sistema: banca, seguros, gestión hospitalaria, electrónica, robótica, administración de empresas, navegación aérea… La lista es interminable. Hoy en día es raro hallar un sector donde un programa informático no esté presente.

Todos los dominios ya son de por sí difíciles de comprender pero, además, ningún cliente va a acudir a una empresa solicitando un sistema que informatice un proceso sencillo, fundamentalmente, porque la inversión sería muy difícil de amortizar. Cuando lo hace, suele proporcionar las características del software como una larga lista de requisitos –los requisitos del sistema– algunos de los cuales son contradictorios y están llenos de lagunas. Estos problemas surgen del muy diferente idioma que emplea el cliente y el ingeniero de software. Los usuarios no saben expresar sus necesidades de forma que los informáticos podamos entenderlas porque usan el lenguaje de su dominio. Sin embargo, debemos tener muy claro que es nuestra responsabilidad comprenderles o guiarles de tal modo que podamos hacerlo.

La complejidad del dominio desaparece cuando el informático hace aplicaciones para informáticos, como un entorno de desarrollo integrado. Esta es la situación típica de otras profesiones ya que no es necesario conocer un dominio adicional para poder llevar a cabo el trabajo.

Brooks [2] califica la complejidad del dominio como arbitraria porque no está basada en principios ni en leyes como, por ejemplo, las leyes de la física. Los ingenieros de software no somos los únicos profesionales que tenemos que tratar con la complejidad. Los físicos también trabajan con objetos terriblemente complejos. La diferencia entre nuestro trabajo y el suyo radica en que los físicos tienen la firme convicción de poder hallar teorías, es decir, principios que expliquen un conjunto de hechos y leyes basadas en ellos. Sin embargo, nosotros tenemos que tratar con la libre voluntad de unas organizaciones y con unos sistemas creados por gente muy diferente, lo que implica que, incluso dentro del mismo dominio, debamos tratar con requisitos distintos. Un sistema de gestión bancaria para un gran cliente no implica que todos los bancos del mundo puedan usarlo porque cada uno establece sus propios procesos. No hay leyes similares a las de la física en los dominios; y cuando pareciera que pudieran existir –como en la navegación aérea– podremos toparnos con un cliente que nos pida que el sistema muestre el rumbo con un decimal y otro sin él.

La complejidad del dominio no acaba en saber qué hay que hacer porque, junto con los requisitos funcionales, el diseño del sistema siempre tendrá que tener en cuenta requisitos como el rendimiento, el coste o la facilidad de uso, es decir, los requisitos no funcionales. Por si fuera poco, una vez que conseguimos tener claros todos los requisitos, tenemos que asumir que parte de ellos va a cambiar a lo largo del desarrollo del proyecto. Esto es algo inevitable. Según se va avanzando y se prototipa la interfaz gráfica de usuario, se elaboran documentos de diseño o se entrega una versión preliminar tras una iteración, los clientes empiezan a ver claras las cosas y es entonces cuando sugieren cambios o matizan sus propuestas iniciales.

En resumen, gran parte de la complejidad del software deriva de la complejidad arbitraria procedente del dominio y es la principal razón [5] por la que un proyecto acaba pasado el plazo, con sobrecoste o llega a cancelarse.

La variabilidad

El software está constantemente sometido a los cambios. Cierto es que otros productos, como los teléfonos inteligentes o los coches, también lo están. Sin embargo, los productos manufacturados rara vez sufren cambios tras su fabricación sino que son reemplazados por productos distintos o se introducen modificaciones en su diseño para fabricar modelos ligeramente diferentes pero que, por supuesto, tiene otro número de serie.

Las modificaciones que tan frecuentemente padece el software vienen dadas porque el software de un sistema encarna su función y es la función la que se quiere cambiar. Un cajero automático, por poner un ejemplo, es un sistema con partes mecánicas, electrónica y software; a la hora de ampliar su función será el último quien reciba el mayor impacto del cambio, cuando no el cien por cien.

Otro aspecto, más obvio, es lo maleable y flexible que es el software. Su desarrollo es puramente intelectual y para cambiarlo no hace falta desechar piezas físicas, otra razón por la cual el software del cajero será el más afectado por el cambio. Esta característica resulta muy atractiva para el ingeniero pues puede crear casi cualquier cosa a cualquier nivel de abstracción. No es raro ver en nuestra profesión como se reinventa la rueda una y otra vez; unas veces por puro interés de las empresas tecnológicas, otras por la carencia de estándares y componentes universales.

Todo el software de calidad cambia y lo hace porque sus usuarios piden la mejora de sus funciones o, sencillamente, características adicionales. Por otra parte, los productos con largos ciclos de vida deben adaptarse a los cambios que acontecen tanto en el dominio de la aplicación como en el mundo de la electrónica: pantallas a más resolución con una relación de aspecto diferente, procesadores de 64 bits en lugar de 32, nuevos dispositivos de almacenamiento, etc.

La invisibilidad

El software en invisible e intangible y, como tal, no ocupa espacio. Las cosas que se fabrican o construyen, pese a que no pueden verse hasta el final del proceso, disponen de abstracciones geométricas que ayudan a comprenderlas. Los planos de un puente, el diseño de un coche o los esquemáticos en la electrónica son algunos ejemplos de estas abstracciones sobre las que se trabaja para detectar incongruencias o ausencias.

El software también se diseña –aunque parece que algunos lo han olvidado– pero en el momento que nos ponemos a diseñar, nos percatamos de que es imposible hacerlo con un tipo de plano. Necesitamos muchos tipos para poder mostrar el diseño desde diferentes puntos de vista y, además, existe una superposición de todos ellos. Esto no sería mayor problema si esa superposición pudiera articularse en el plano o en el espacio –como sucede en el mundo de la construcción– pero no es así. Debemos mostrar el software desde un punto de vista funcional, estático, dinámico, temporal y físico y hacerlo a muchos niveles de abstracción, algo que conduce a crear enormes jerarquías. Diseñar software es como querer representar algo en cinco dimensiones.

Nuestra gran desventaja es que, incluso cuando el software está terminado, sigue siendo muy difícil establecer la relación entre los planos y el producto porque éste es invisible. La interfaz de usuario es solo la punta del iceberg de una aplicación y algunas ni la tienen. Pocas profesiones requieren de una capacidad de abstracción tan alta.

La gestión

La tarea principal del equipo de desarrollo es hacer fácil lo difícil para los usuarios de un dominio determinado y, de esta forma, aislarles de su vasta complejidad.

Aunque nuestro objetivo debe ser escribir el menor número de líneas de código posible, haciendo uso de la reutilización de diseños previos o empleando componentes de terceros, el caso es que el software siempre tendrá miles y miles de líneas de código. Un sistema, incluso uno pequeño, es imposible de comprender completamente por una sola persona, de ahí la necesidad de contar con un equipo que debería ser tan reducido como sea posible. Cuanto más grande sea el equipo, más difícil se vuelve la coordinación y la comunicación, más aún si el equipo está disperso geográficamente. Toda persona que se incorpora a un proyecto necesita tiempo para ser productiva y tiempo para aprender sobre el trabajo que ya está hecho. Además, no se pueden paralelizar las tareas a realizar ad infinitum porque siempre habrá caminos críticos que impedirán hacerlo. Recuerde la célebre frase de Brooks:

«Nueve mujeres no pueden tener un bebé en un mes».

Independientemente del tamaño, siempre hay retos que afrontar. El gran reto de la jefatura técnica de un proyecto de desarrollo de software es siempre el mantenimiento de la unidad e integridad del diseño.

Un sistema discreto

Para un instante dado, los datos, la dirección de ejecución y el estado de la pila constituyen el estado actual de una aplicación. Dado que el software se ejecuta en ordenadores, es un sistema con un número finito de estados discretos pero que puede llegar a ser enorme en grandes programas. Cuando depuramos paso a paso una aplicación, cada uno de ellos da lugar a un cambio de estado o más, ya que solemos trabajar con lenguajes de alto nivel.

Trabajo en el campo de los enlaces de datos tácticos (sistemas de comunicaciones) donde el tratamiento de uno solo de sus protocolos implica lidiar con una cantidad de información asombrosa, pese a que el software resultante sea una aplicación de pequeño tamaño. Un protocolo, pongamos por caso, con 50 mensajes y 30 campos por mensaje, donde cada campo pueda almacenar hasta 20 valores diferentes, implica el procesamiento de 30 000 valores distintos. Además, la aplicación debe tener en cuenta que no todos los valores son válidos, hay valores que se tienen o no en cuenta en función del contenido de otros campos… En definitiva, hay que tener presente toda la lógica de negocio para realizar un correcto tratamiento de la información. Calcular el número de estados de una aplicación como ésta raya lo irracional.

Por otra parte, los sistemas analógicos son sistemas continuos. Si se deja caer una piedra al suelo, sabemos que debido a las leyes de la física, como la fuerza de la gravedad y la fuerza debida al peso, la piedra terminará en el suelo. Si la física fuera un sistema discreto sería posible que, ante cualquier evento externo, la piedra se quedara parada a medio camino; un hecho que sin duda podría suceder en una simulación por ordenador poco depurada de este fenómeno.

Resulta absolutamente imposible probar todos los estados discretos por los que puede pasar una aplicación, ni tenemos los medios ni la capacidad intelectual para hacerlo. De ahí que las pruebas de un sistema siempre se acepten basándose en cierto grado de confianza.

La complejidad accidental del software

Hace décadas se programaba en lenguaje ensamblador con rudimentarias herramientas –incluso se ensamblaba el código a mano– y ordenadores de capacidad muy limitada que almacenaban los programas y los datos en dispositivos con tiempos de acceso que hoy nos desesperarían.

El crecimiento espectacular de la potencia de los ordenadores, los lenguajes de alto nivel –en especial los orientados a objetos– o los entornos de desarrollo integrado han contribuido a facilitar enormemente nuestra profesión y aumentar significativamente la productividad.

Sin embargo, esta reducción evidente de la complejidad del software es lo que Brooks denomina complejidad accidental, ya que no resuelve la complejidad inherente. El ingeniero actual, como el ingeniero de los años setenta, debe enfrentarse a la complejidad arbitraria de los dominios, las miles de líneas de código y estados, la presión al cambio, la complejidad del diseño y la gestión de los proyectos. Si nos detenemos a hacer un mínimo análisis, incluso podría afirmarse que la reducción de la complejidad accidental ha hecho aumentar algunos de los parámetros que miden la complejidad inherente, por tanto, cuanto más se reduzca la primera más aumentará la segunda.

Noticias como «Programar no es el trabajo del futuro que creías: cómo esta máquina cambiará el oficio» [3] o «El 80 % de los productos digitales serán creados por personas que no saben programar para 2024, según Gartner» [10] encierran un mensaje equivocado. Estas herramientas o técnicas –bienvenidas sean– servirán para reducir la complejidad accidental, nunca la inherente. Sin embargo, cualquier lego en la materia bien podría creer que hacer software se convertirá en un juego de niños en dos o tres años. Estos titulares, además de engañosos pueden ser hasta peligrosos en manos de directivos mal informados de empresas tecnológicas. Quiero pensar que son una minoría, pero haberlos haylos.

Lo más curioso es que se llevan publicando artículos sobre la programación automática desde los años cuarenta. Aunque su definición ha ido variando a lo largo de las décadas, Parnas [8] concluye que «la programación automática siempre ha sido un eufemismo para referirse a la programación en un lenguaje de mayor nivel que los disponibles entonces». Cuando una máquina sea capaz de comprender los requisitos de un cliente, la inteligencia artificial dejará de ser artificial.

Referencias

[1] Grady Booch. Análisis y diseño orientado a objetos con aplicaciones. Adison-Wesley Iberoamericana, 1996.

[2] Frederick Brooks. «No Silver Bullet: Essence and Accident in Software Engineering». En: IEEE Computer, vol. 20(4) (1987).

[3] Guillermo Cid. Programar no es el trabajo del futuro que creı́as cómo esta máquina cambiará el oficio. El Confidencial. 2021. url: https://www.elconfidencial.com/tecnologia/2021-07-04/programacion-futuro-ia-openai-gpt3-futuro_3159368

[4] Pierre-Jacques Courtois. «On Time and Space Decomposition of Complex Structures». En: Communications of the ACM, vol. 28(6) (1986).

[5] Forbes. 14 Common Reasons Software Projects Fail (And How To Avoid Them). 2020. url: https://www.forbes.com/sites/forbestechcouncil/2020/03/31/14-common-reasons-software-projects-fail-and-how-to-avoid-them

[6] John Gall. Systemantics: How Systems Work & Especially How They Fail (Second Edition). The General Systemantics Press, 1986.

[7] The Standish Group. CHAOS Report. 2015. url: https://www.slideshare.net/Mateuszeromski/chaos-report-2015

[8] David L. Parnas. «Software Aspects of Strategic Defense Systems». En: American Scientist (1985).

[9] Eberhardt Rechtin. «The Art of Systems Architecting». En: IEEE Spectrum, vol. 29(10) (1992).

[10] Pablo Rodríguez. El 80 % de los productos digitales serán creados por personas que no saben programar para 2024, según Gartner: el código bajo abre las puertas del desarrollo. Xataka Pro. 2021. url: https://www.xataka.com/pro/80-productos-digitales-seran-creados-personas-que-no-saben-programar-para-2024-gartner-codigo-abre-puertas-desarrollo

[11] Herbert Simon. The Sciences of the Artificial. The MIT Press, 1982.

Fotografía de la entrada por Fakurian Design en Unsplash

Entradas relacionadas