Historia y fundamentos de la ingenieria de software
La historia del desarrollo de software puede entenderse como la búsqueda constante de mejores formas de afrontar un problema que apareció casi al mismo tiempo que los propios ordenadores: construir sistemas cada vez más grandes y complejos sin perder el control sobre ellos. En los primeros años de la informática, programar consistía principalmente en conseguir que una máquina ejecutara correctamente una serie de instrucciones. A medida que los sistemas crecieron, esa visión dejó de ser suficiente, porque ya no bastaba con conseguir que el código funcionara: también era necesario encontrar formas sistemáticas de organizar, desarrollar y mantener sistemas cada vez más complejos.
De programar a hacer ingeniería
Durante las décadas de 1950 y 1960, el software comenzó a adquirir una importancia creciente en sistemas científicos, industriales y militares. Los proyectos aumentaban de tamaño y, con ellos, también lo hacían los problemas: retrasos, costes superiores a los previstos, errores difíciles de corregir y sistemas que resultaban cada vez más difíciles de mantener.
Este fenómeno se conoció como la crisis del software, una expresión utilizada para describir las dificultades que encontraba la industria para desarrollar software fiable y económicamente viable a medida que aumentaba su complejidad. El problema no era únicamente que los programadores cometieran errores, sino que las técnicas utilizadas para construir software no evolucionaban al mismo ritmo que los sistemas que se pretendía construir.
A partir de esta situación comenzó a consolidarse la idea de la ingeniería de software como una disciplina que debía aplicar métodos sistemáticos al desarrollo y mantenimiento de software. El cambio de perspectiva es importante. Ya no se trata únicamente de preguntarnos cómo escribir determinado código, sino de comprender qué problema estamos intentando resolver, qué características debe tener la solución, cómo vamos a construirla, cómo comprobaremos que funciona y cómo conseguiremos mantenerla cuando cambien las circunstancias.
Por eso, aunque la programación sea una parte fundamental de la ingeniería de software, no constituye todo el proceso. Un buen desarrollo requiere tomar decisiones antes de escribir código y continuar tomando decisiones después de que ese código haya llegado a producción.
Un proceso que comienza antes del código
Podemos imaginar el desarrollo de software como un proceso que comienza con una necesidad y continúa durante toda la vida del sistema. Primero debemos comprender qué se necesita; después decidir cómo vamos a resolverlo; posteriormente implementar esa solución y comprobar que se comporta como esperamos. Una vez puesta en producción, tendremos que observarla, corregirla y adaptarla a nuevas necesidades.
Este recorrido suele dividirse conceptualmente en actividades como requisitos, diseño, implementación, pruebas, evaluación y mantenimiento. No debemos interpretar estas actividades como compartimentos completamente separados ni como una secuencia rígida en la que una termina definitivamente antes de comenzar la siguiente.
En la práctica, estas actividades se mezclan y se repiten, porque al diseñar podemos descubrir que un requisito no estaba suficientemente definido, durante la implementación podemos descubrir una limitación técnica que obliga a revisar el diseño y las pruebas pueden revelar que nuestra interpretación del requisito era incorrecta.
Esta característica resulta especialmente importante porque el conocimiento sobre el sistema no aparece todo de golpe. Se construye progresivamente mientras el equipo comprende el problema, prueba soluciones y recibe información de los usuarios y del propio sistema.
Los requisitos: comprender qué necesitamos construir
Antes de decidir cómo construir una solución necesitamos comprender qué debe resolver. Esta actividad pertenece a la ingeniería de requisitos, que se ocupa de identificar, analizar, documentar y gestionar las necesidades que debe satisfacer un sistema.
Un requisito puede expresar una funcionalidad concreta que el sistema debe proporcionar. Por ejemplo, una aplicación podría necesitar permitir que un usuario consulte los centros asociados a un determinado estudio. Pero también existen requisitos relacionados con características como el rendimiento, la seguridad, la disponibilidad o la facilidad de mantenimiento.
Podemos distinguir así entre requisitos funcionales, que describen comportamientos o capacidades que debe proporcionar el sistema, y requisitos no funcionales, que describen propiedades o restricciones bajo las cuales debe funcionar.
Sin embargo, hablar de requisitos únicamente como una lista de funcionalidades puede resultar engañoso. Detrás de cada requisito existe una necesidad de negocio o de usuario que debemos comprender. Una aplicación puede implementar exactamente lo que está escrito y, aun así, no resolver correctamente el problema que originó su desarrollo.
Por eso, una parte importante del trabajo de ingeniería consiste en hacer preguntas, detectar ambigüedades, identificar restricciones y entender quién utilizará el sistema y con qué propósito. Cuanto mejor comprendamos el problema, más posibilidades tendremos de construir una solución adecuada.
El diseño: decidir cómo resolver el problema
Una vez comprendido qué necesitamos conseguir aparece una nueva pregunta: cómo vamos a construir la solución.
El diseño de software consiste en tomar las decisiones necesarias para transformar los requisitos en una solución construible. Algunas de esas decisiones afectan a la estructura general del sistema y forman parte de su arquitectura de software; otras son más concretas y determinan cómo se organizarán determinadas funcionalidades, componentes o estructuras de datos.
En esta etapa aparecen decisiones como dónde almacenar la información, cómo se comunicarán los diferentes componentes, dónde debe situarse una determinada responsabilidad o qué dependencias tendrá una funcionalidad respecto a otras partes del sistema.
Estas decisiones no son puramente técnicas. Una arquitectura puede facilitar el crecimiento futuro del sistema, pero introducir mayor complejidad inicial. Una solución sencilla puede ser suficiente para las necesidades actuales, pero resultar más difícil de ampliar posteriormente. Diseñar software implica, por tanto, trabajar con compromisos entre diferentes necesidades y restricciones.
Una consecuencia importante es que las decisiones de diseño tienen una vida más larga que muchas líneas concretas de código. El código puede cambiar con relativa facilidad, mientras que una decisión arquitectónica puede condicionar durante años la evolución de una aplicación.
La implementación: convertir las decisiones en software
La implementación es probablemente la actividad que más asociamos tradicionalmente con el desarrollo de software: escribir código, configurar componentes y construir el sistema que hemos diseñado.
Sin embargo, contemplar la implementación de forma aislada puede llevarnos a una visión demasiado limitada de la profesión. El código es el resultado visible de muchas decisiones anteriores, y cuando un desarrollador implementa una funcionalidad está materializando requisitos y decisiones de diseño que deberían haber sido comprendidos previamente.
Esto tampoco significa que el diseño deba completarse por entero antes de escribir una sola línea de código. En muchos proyectos descubriremos durante la implementación que determinadas decisiones necesitan ser revisadas. Precisamente por eso los procesos modernos tienden a favorecer ciclos cortos de construcción y aprendizaje.
Aquí adquiere especial importancia la calidad interna del software. Dos programas pueden proporcionar exactamente el mismo resultado al usuario y, sin embargo, tener costes de mantenimiento completamente diferentes. La estructura del código, la claridad de sus responsabilidades, la gestión de dependencias y la facilidad para modificarlo condicionan directamente la evolución futura del sistema.
Cuando estas decisiones se posponen constantemente para resolver problemas inmediatos, puede aparecer la denominada deuda técnica: decisiones que permiten avanzar más rápidamente en el presente pero que pueden incrementar el coste de modificar el sistema posteriormente.
Las pruebas: comprobar que el sistema hace lo que debe
Una vez construido el software necesitamos obtener evidencias de que funciona correctamente. Para ello utilizamos las pruebas de software, que permiten comprobar el comportamiento del sistema bajo diferentes condiciones.
Las pruebas pueden realizarse a diferentes niveles. Podemos comprobar una pequeña unidad de código, la interacción entre varios componentes o el comportamiento de todo el sistema desde la perspectiva del usuario. También podemos comprobar distintos aspectos del sistema, desde sus funcionalidades hasta características como el rendimiento o la seguridad.
Pero existe una distinción especialmente importante entre verificación y validación. La verificación se centra en comprobar que estamos construyendo correctamente el producto según las especificaciones establecidas. La validación intenta responder a una pregunta diferente: si aquello que estamos construyendo es realmente la solución adecuada para la necesidad que pretendíamos resolver.
Las pruebas permiten comprobar que el sistema se comporta de acuerdo con los requisitos, pero esta comprobación no garantiza por sí sola que hayamos definido correctamente esos requisitos.
Esta diferencia explica por qué un sistema puede superar todas sus pruebas técnicas y, aun así, no resolver correctamente el problema que originó su desarrollo. Podemos construir correctamente algo que no era realmente lo que necesitábamos construir.
La evaluación: mirar el sistema desde una perspectiva más amplia
La evaluación amplía todavía más nuestra perspectiva. Un sistema puede funcionar técnicamente y cumplir sus requisitos, pero todavía debemos preguntarnos si está proporcionando el resultado esperado.
Podemos evaluar aspectos como el rendimiento, la seguridad, la fiabilidad, la facilidad de mantenimiento, los costes de operación o la capacidad del sistema para adaptarse a nuevas necesidades. También podemos analizar si realmente está aportando el valor que justificó su construcción.
Un sistema puede funcionar correctamente desde el punto de vista técnico y, aun así, dejar de ser adecuado para la necesidad que debe resolver.
Esta evaluación no tiene por qué producirse únicamente al final de un proyecto. En un desarrollo iterativo podemos utilizar la información obtenida en cada ciclo para revisar nuestras decisiones y determinar si debemos continuar por el mismo camino o modificarlo.
De esta manera, evaluación y aprendizaje se convierten en partes naturales del proceso de ingeniería. El equipo no sólo construye software: construye conocimiento sobre el problema y utiliza ese conocimiento para mejorar la siguiente decisión.
Del modelo en cascada al desarrollo iterativo
La forma de organizar estas actividades también ha evolucionado a lo largo de la historia. Uno de los modelos clásicos fue el modelo en cascada, en el que las diferentes fases del desarrollo se organizaban de manera secuencial.
Este modelo resulta sencillo de comprender y puede ser adecuado cuando los requisitos son relativamente estables y conocidos. Sin embargo, muchos proyectos de software no tienen esas características. Cuando los requisitos cambian, cuando existen incertidumbres técnicas o cuando el usuario necesita descubrir progresivamente qué solución necesita realmente, resulta difícil definirlo todo por adelantado.
Los enfoques iterativos y, posteriormente, las metodologías de desarrollo ágil de software, ponen mayor énfasis en construir pequeñas partes del sistema, obtener información y utilizarla para orientar las siguientes decisiones.
En lugar de realizar cada actividad una única vez, podemos recorrer repetidamente el ciclo de comprender una necesidad, diseñar una solución, implementarla, probarla y evaluarla.
Esto no significa que desaparezcan los requisitos, el diseño, la implementación o las pruebas. Lo que cambia es la forma de relacionarlos. El proceso pasa a entenderse como una sucesión de ciclos en los que el conocimiento adquirido permite revisar las decisiones anteriores.
El mantenimiento: cuando comienza la verdadera vida del software
Cuando una aplicación llega a producción podría parecer que el proyecto ha terminado. En realidad, desde el punto de vista de la ingeniería de software, comienza una etapa especialmente importante.
El mantenimiento de software incluye la corrección de errores, la adaptación a nuevos entornos, la incorporación de funcionalidades y todas aquellas modificaciones necesarias para mantener el sistema útil a lo largo del tiempo.
El mundo que rodea al software cambia. Cambian las necesidades de los usuarios, las regulaciones, los sistemas operativos, las infraestructuras, las tecnologías y las integraciones con otros sistemas. Incluso cuando los requisitos de negocio permanecen estables, las condiciones técnicas pueden obligarnos a modificar el software.
El software debe seguir adaptándose después de su puesta en producción, porque las necesidades y el entorno en el que funciona también evolucionan.
Por eso conceptos como mantenibilidad, simplicidad, modularidad y bajo acoplamiento tienen una importancia que va más allá de la estética del código. Una decisión que parece pequeña durante el desarrollo puede determinar cuánto esfuerzo será necesario para realizar un cambio dentro de varios años.
Ingeniería de software frente a programación
Llegados a este punto, podemos establecer una diferencia fundamental. Programar consiste en escribir instrucciones que una máquina puede ejecutar. La ingeniería de software engloba un problema mucho mayor: comprender una necesidad, definir qué debe hacer el sistema, diseñar una solución, implementarla, verificarla, validarla, ponerla en funcionamiento y mantenerla durante su vida útil.
Esto no reduce la importancia de la programación. Al contrario, un buen conocimiento de programación sigue siendo una de las herramientas fundamentales del ingeniero de software. Lo que cambia es el contexto en el que se utiliza.
La programación es una actividad fundamental dentro de la ingeniería de software, pero la ingeniería de software abarca todo el proceso necesario para construir y evolucionar un sistema.
Esta distinción también ayuda a comprender por qué, a medida que aumenta la experiencia profesional, las preguntas que se plantean los desarrolladores suelen cambiar. Al principio es natural concentrarse en cómo implementar una funcionalidad. Posteriormente aparecen preguntas sobre dónde debería vivir esa funcionalidad, qué otras partes del sistema afecta, qué requisitos existen realmente o cómo evolucionará la solución dentro de unos años.
La progresión no consiste simplemente en aprender más lenguajes o más herramientas. Consiste también en ampliar el nivel de abstracción desde el código hacia el sistema y, finalmente, hacia el problema que el sistema intenta resolver.
Una disciplina centrada en gestionar complejidad y cambio
Vista en conjunto, la historia de la ingeniería de software puede interpretarse como la evolución de las técnicas utilizadas para gestionar dos problemas fundamentales: la complejidad y el cambio.
La complejidad aparece porque los sistemas modernos están formados por numerosas piezas que interactúan entre sí. El cambio aparece porque las necesidades, las tecnologías y el entorno nunca permanecen completamente estáticos.
Requisitos, diseño, implementación, pruebas, evaluación y mantenimiento no son, por tanto, simples etapas administrativas de un proyecto. Son diferentes actividades que nos permiten comprender el problema, construir una solución, comprobarla y mantenerla a medida que cambian las circunstancias.
Desde esta perspectiva, aprender ingeniería de software significa aprender a tomar decisiones conscientes sobre todo el ciclo de vida de un sistema. Significa comprender el problema antes de resolverlo, reconocer que las decisiones técnicas tienen consecuencias, comprobar nuestras hipótesis mediante pruebas y evaluación y diseñar pensando en un futuro que inevitablemente traerá nuevos requisitos.
Aprender ingeniería de software significa pasar de pensar únicamente en cómo escribir código a comprender cómo construir, evaluar y evolucionar sistemas completos.