La semana pasada acudí a la IEEE International Conference on Software Maintenance para presentar un artículo. Durante la conferencia hubo una sesión de “mitos” en Ingeniería del Software.
Uno de los mitos que se discutieron es si el código más complejo contiene más defectos (a igual tamaño). Casualmente, hace unos meses escribí un artículo (que, ampliado, es uno de los capítulos de mi tesis) que aborda parcialmente este problema.
Las conclusiones del artículo fueron que diferentes métricas de tamaño y complejidad están altamente correlacionadas, y por lo tanto proporcionan la misma información. Respecto al número de defectos, esto querría decir que cualquiera de las métricas estudiadas sería tan válidad como otra para predecir defectos, y que por tanto, el número de defectos se puede predecir simplemente con tamaño.
Esto tiene dos implicaciones: o bien el código más complejo no contiene más defectos, o bien las métricas que se suelen usar para medir complejidad no están midiendo realmente complejidad.
Yo me inclino más por la segunda opción. Las métricas clásicas sobre complejidad no son buenas.
Esto nos lleva a la cuestión de qué es la complejidad del software, y cómo medirla. Un amigo está trabajando en el tema, intentando medir complejidad usando una métrica curiosa. Él supone que cuánto más variable sea la estructura del código, más esfuerzo será necesario para entenderlo, y por tanto más complejo será. Está trabajando en un artículo, que publicará en breve, en el que mide complejidad como la varianza de la distribución del número de espacios de indentación del código. Por ejemplo, la declaración de una función tiene 0 espacios, pero el código dentro de la función tendrá 2 ó 3. Si hay bucles, condicionales, etc, el indentado variará. Cuánto mayor sea esta variación, mayor será la complejidad. Ésa es su hipoesis. Así que ha medido eso para una muestra de ficheros. Luego compara su métrica con otras medidas de complejidad, y comprueba que existe correlación. Su conclusión es que esa métrica es por tanto buena para medir complejidad. El problema es que para hacer este test ha usado las métricas que yo he comprobado ya que están altamente correlacionadas con tamaño.
Así que a pesar de que es una idea ingeniosa, creo que habrá que seguir mirando una manera sencilla de medir la complejidad del software.
Para buscar una métrica adecuada, tenemos que pensar que un código más complejo es el que demanda más esfuerzo de nosotros a la hora de entenderlo y ser capaces de modificarlo. Teniendo en cuenta esto, mi primera opción es mirar a las dependencias entre ficheros de código fuente. Lo que más odio cuando leo un programa grande, es tener que andar dando saltos de fichero en fichero siguiendo el flujo del programa, o una determinada parte del programa. Además, si cuando cambias algo en una parte rompes cosas en muchos otros sitios, eso significa que el software es más complejo porque requiere más esfuerzos para entenderlo y cambiarlo.
Pero no me gusta demaiado esta métrica, porque en realidad es una medida global de la complejidad. Esa métrica no tendría sentido para un fichero aislado. Y sin embargo entender un solo fichero puede ser tan o mucho más complicado que entender un conjunto de ficheros.
Así que le he seguido dando vueltas al tema. Otra de las sesiones sobre mitos iba sobre clonado de código. Normalmente, se piensa que tener clones de código en un programa es malo. Si tienes que cambiar algo por que hay un fallo en una porción de código, y esa porción está repetida en muchos sitios, tendrás que ir buscándola para arreglar el fallo en muchos sitios diferentes.
Esa es la teoría. En la práctica, los desarrolladores clonan código a modo de patrones. Existen varias razones. Una es que el código sea tan complejo como para entenderlo del todo, y por tanto es mejor copiarlo, pegarlo en otro sitio, y hacer las modificaciones que se necesiten. Otra es que normalmente los ficheros tienen dueños, así que si necesitas una parte de código que no puedes controlar porque no te pertenece, la copias en otro sitio para controlarla, y asegurarte contra cambios que no puedes controlar.
En cualquier caso, parece que en proyectos de software libre hay más clonado del que en teoría sería deseable. Y sin embargo se hace precisamente para poder gestionar la complejidad del proceso de desarrollo. El clonado ayuda a los desarrolladores a dedicar menos esfuerzo a cambiar o entender un programa.
El clonado no es más que información redundante dentro de un programa. No tiene por qué ser exactamente código copiado. Puede haber modificaciones. Por ejemplo, los alumnos cuando copian las prácticas de programación, hacen clones. No son exactamente iguales, pero realizan la misma función. Un ejemplo tonto de clonado sería cambiar un bucle for por uno while con un contador.
Por tanto, podemos asumir que es esa redundancia la que facilita la tarea, y que la ausencia de redundancia sería una medida de la complejidad del programa. Una manera simple de medir redundancia en un programa (en cualquier fichero en realidad) es comprimirlo. Si el tamaño del fichero comprimido es muy pequeño comparado con el fichero original, es que existía mucha redundancia en el fichero original. Este método tiene problemas con el código, porque clones como cambiar un bucle for por uno while no sería detectado. Quizás sería más eficiente si en vez de hacerlo a nivel de código fuente se hiciera a nivel de código objeto (porque el código máquina de los clones se parecerá mucho entre sí).
Por tanto, algún tipo de métrica que relacione el tamaño del fichero (código fuente o código objeto) con el tamaño del fichero comprimido nos está dando una idea de la cantidad de redundancia en el fichero, y por tanto, quizás de la complejidad del programa para ser entendido o modificado por una persona.
¿Tienes experiencia programando? ¿Qué consideras tú que es una buena medida de la complejidad de un programa? ¿Qué opinas de las propuestas que menciono para medir la complejidad?
Yo ponderaría los resultados con las siguientes variables: Proporción de líneas de comentarios respecto al total del programa, proporción de nombres de funciones o de variables construidos como palabras compuestas de un diccionario (ya que son más legibles que otras en formato abreviado), y cosas así. Comparar el código objeto tiene un problema, y es que un programa ofuscado y otro con nombres de funciones o variables con sentido que generen el mismo código serán vistos en comparación como dos códigos de complejidad semejante. Hay que distinguir la complejidad del código fuente (que parece ser que es lo que te interesa) de la complejidad del código objeto.
Me gusta mucho más el concepto de búsqueda de patrones. ¿No sería genial disponer de una herramienta que, mediante una pequeña base de datos con los patrones más comunes, y algo de heurística, fuese capaz de detectar cuántos patrones típicos utiliza una aplicación? Cuando se usan patrones, un programador decente será capaz de detectarlos fácilmente y entender rápidamente el código. Es más, incluso un programador que desconozca un patrón determinado podría aprenderlo sin demasiada dificultad si lo encuentra por doquier en el código del programa, de manera que acabará acostumbrándose a interpretarlo en pocos segundos.
Otro detalle está en la expresividad de los lenguajes. Hay lenguajes que, en pocas líneas, son capaces de expresar funciones que otros requerirían centenares de ellas para lograr los mismos resultados. No tendría mucho sentido comparar resultados de complejidad de un programa escrito en C, con otro programa escrito en Ruby, teniendo en cuenta únicamente los aspectos indicados en el artículo. No quiero decir nada si a esto sumamos que, con un supuesto como este, la aplicación en Ruby utiliza técnicas de metaprogramación, o hace que no debas preocuparte por la desasignación de memoria pues para ello está el recolector de basura. Tampoco se pueden comparar los resultados en general obtenidos por lenguajes procedurales con los obtenidos por lenguajes funcionales, orientados a objetos, y otras hierbas.
La verdad es que has escogido un tema dificilillo para tu tesis
Admiro mucho el trabajo que estás haciendo.