archivo

Archivo de la etiqueta: complejidad ciclomática

Para Bob C. Martin un software que presenta algunos de los siguientes defectos tiene un mal diseño:

1) Rigidez: Es complicado realizar cambios porque cada cambio afecta a otras partes del sistema.
2) Fragilidad: Cuando realizas un cambio, partes insospechadas del sistema empiezan a fallar.
3) Inmovilidad: Es difícil reutilizar código en otra aplicación porque no puede ser separado de la aplicación en la que se está usando.

Es importante incidir en que Martin se está centrando en aspectos de diseño y de arquitectura y no en aspectos de programación (pese a que al final estos problemas se hagan evidentes en el proceso de desarrollo y en las actividades de mantenimiento o evolución del sistema), por ese motivo los defectos que plantea están relacionados con factores como la modularidad, el alto acoplamiento, la baja cohesión y la alta complejidad ciclomática (entre otros).

Un buen diseño se valora cuando has trabajado con sistemas que no lo tienen, en los cuales realizar cualquier tarea de mantenimiento se encuentra con una resistencia que hace que los costes se disparen, sin que por ello se garantice que el producto va a pasar a producción con menos errores que los que tenía antes.

Este antipatrón se basa en la utilización dentro de un mismo sistema de diferentes «clases de propósito general», «clases hombre-orquesta» o «clases multicasuística» en las cuales están definidos una amplia gama de comportamientos con el objeto de poder ser utilizados por otras.

También es frecuente encontrarlo en implementaciones de componentes en los que se delegan funcionalidades de otros, con el objeto de dar un amplio rango de alternativas posibles al cliente.

El problema no es dar esas facilidades, sino que el problema se encuentra en la complejidad que se le añade al código como consecuencia de las mismas (generalmente alto acoplamiento, baja cohesión, alta complejidad ciclomática, etc…).

En el antipatrón «objeto todopoderoso«, la funcionalidad de la aplicación pivota generalmente sobre una de estas clases, mientras que en la navaja suiza se trata de diferentes clases (generalmente, de utilidad) que contiene el sistema.

Este antipatrón es una mala práctica de programación y se llega a él a través de la implementación de métodos que intentan ser lo suficientemente flexibles como para ser adaptado su comportamiento a multitud de circunstancias, sobrepasando el umbral de una mantenibilidad adecuada del mismo (elevado número de parámetros donde la mayoría de ellos a veces se usan y otras no, alta complejidad ciclomática, etc…).

Presenta una analogía con el antipatrón «objeto todopoderoso«, pero teniendo como protagonista a un método y no a una clase.

Un claro síntoma de que un código ha sido desarrollado por personal que todavía necesita más experiencia y cualificación es la existencia de clases que concentran gran parte de funcionalidad del sistema, de manera que el sistema pivota alrededor de las mismas (generalmente una de ellas).

Estas clases presentarán como mínimo una baja cohesión, tendrán una complejidad ciclomática superior a la media del resto de clases y probablemente mostrarán también un alto acoplamiento.

En mi organización vamos a realizar un estudio de una serie de sistemas de información (entre el 50 y el 75% aproximadamente de los mismos) con el objeto de detectar en ellos una serie de clases que pueden resultar conflictivas desde el punto de vista de la mantenibilidad.

Cualquier clase que cumpla alguna de las siguientes condiciones la consideraremos sospechosa (son solo métricas, que pueden encender determinadas alarmas, pero después resulta aconsejable revisar si existe justificación o no en que tomen esos valores) y será objeto de estudio con la finalidad de tomar una decisión sobre la necesidad de su refactorización:

1) Acoplamiento. Utilizaremos como base la métrica RFC de Chidamber y Kemerer.

RFC(clase)>=50 y RFC(clase)/Nº Métodos(clase)>=10

2) Cohesión. Utilizaremos como base la métrica LCOM4 de Chidamber y Kemerer.

LCOM4(clase)>=2

3) Complejidad Ciclomática de Thomas J. McCabe.

Complejidad Ciclomática(clase)/Nº Métodos(clase)>=10

Los valores anteriores por cada clase se obtendrán utilizando las métricas obtenidas a partir de Sonar.

El puglin de Sonar Quality Index proporciona otra métrica agregada (resultado de otras métricas) para obtener un valor que indique la calidad del software. En este caso lo que se pretende es obtener un valor entre 0 y 10.

Particularmente me gusta más SIG MM ya que se centra en la mantenibilidad que es lo que realmente me preocupa sobre el software que nos entregan, pero todo es cuestión de gustos y de lo que cada uno valore lo que te calcula la métrica y lo claro que te exprese los resultados. Desde mi punto de vista este indicador da demasiado peso a la verificación de reglas PMD y pone casi al mismo nivel la verificación de estilos que la complejidad ciclomática y no otorga ningún valor al acoplamiento o a la cohesión (algo de lo que también adolece SIG MM).

Para su cálculo se basa en la obtención previamente de cuatro métricas:

– Violaciones de código: basada en los resultados obtenidos mediante la aplicación de las reglas PMD y se calcula a partir de la siguiente fórmula:

Coding Violations = (Blocker * 10 + Critical * 5 + Major * 3 + Minor + Info) / validLines

donde validLines es el número total de líneas de código de la aplicación menos el código duplicado. Como puede darse el caso de que la métrica código duplicado supere al número de líneas de código cuando ocurre esto el valor de validLines es 1. Como casi todas las métricas de Quality Index se hacen en relación al número de líneas válidas la influencia de la duplicidad de código en los resultados finales es importante (como también lo es en el cálculo de otras métricas como Technical Debt o SIG MM, algo que me parece totalmente razonable. Esto va a provocar en consecuencia que soluciones basadas en generadores de código, salvo que éstos estén muy depurados van a verse muy perjudicadas por esta métrica.

– Complejidad: basada en los resultados obtenidos mediante la obtención de la complejidad ciclomática de los diferentes métodos y se calcula a partir de la siguiente fórmula:

Complexity = (Complexity>30 *10 + Complexity>20 * 5 + Complexity>10 * 3 + Complexity>1) / validLines

Como se puede apreciar se le da un peso importante a métodos con una complejidad ciclomática alta que son aquellos que al presentar un mayor número posible de caminos lógicos hace que sean más complicados de mantener

– Cobertura: Se obtiene directamente a partir de la cobertura de código por pruebas unitarias (mirando un comentario del código del plugin (clase CoverageDecorator.java) no tiene en cuenta para el cálculo de esta métrica las líneas duplicadas.

– Violaciones de estilo: basada en los resultados obtenidos mediante la aplicación de las reglas PMD y se calcula a partir de la siguiente fórmula:

Style = (Errors*10 + Warnings) / ValidLines * 10

Una vez calculadas las métricas ya se puede tener el valor de Quality Index:

QI = 10 – (4.5 * Coding – 2 * Complexity – 2 * Coverage -1.5 * Style)

El plugin además de calcularte el valor global, te indica el valor de las 4 métricas (ejes) llevados a la escala correspondiente en función de su peso en la fórmula del cálculo de la métrica, representando en rojo la proporción que incumple y en verde la proporción que cumple.

Se codifica, se construye la aplicación, pero en demasiadas ocasiones se tiene en cuenta como fin único la entrega del proyecto, dejando al margen o dedicándole menos importancia a una programación pensando en futuros mantenimientos.

La dificultad de los mantenimientos depende de muchos factores y algunos de ellos están por encima en importancia de una codificación clara y de calidad, como por ejemplo la búsqueda de una alta cohesión y un bajo acoplamiento, lo cual no debería ser una excusa para olvidarnos de este asunto ya que no se trata solo de que se pueda comprender lo que se ha programado (que no es poco) sino que un código poco cuidado probablemente influirá en una mayor complejidad ciclomática de la aplicación (además de a la mayoría de las variables que determinan la deuda técnica de la aplicación) dificultando, en consecuencia, a la mantenibilidad.

Generalmente nos solemos acordar de todo esto cuando toca realizar el mantenimiento del sistema de información y nos encontramos con que la forma en que se ha codificado hace que se requiera un mayor esfuerzo y en consecuencia coste. Por este motivo importa y mucho como se codifica.

El problema de todo esto es que resulta complicado detectar este tipo de problemas en tiempo de desarrollo ya que obliga a estar encima y revisar el código, lo que puede hacer que casi cueste más el collar que el perro. Por tanto, tenemos que utilizar otras estrategias que faciliten la localización de estos problemas, como es por ejemplo estudiar variables que se pueden incrementar indirectamente por una mala programación (se puede realizar una revisión periódica de las métricas que devuelve Sonar) y complementarlo, si es posible, con la revisión, también periódica, de una muestra de clases de la aplicación.

Siendo realista, veo complicado que una empresa de desarrollo, salvo que las deficiencias encontradas en el proceso de verificación interno sean serias, se pongan a rehacer a módulos ya codificados, pero por lo menos si se aplican estas políticas se puede reconducir la tendencia en el desarrollo y que el proyecto se encuentre dentro de unos umbrales de calidad aceptables (los cuales en algunos casos podrán ser exigidos y verificados por el cliente y provocar un rechazo en la entrega y la consiguiente necesidad de refactorizar partes de la aplicación lo cual se volverá en contra del proveedor ya que tendrá que sufrir en sus carnes las consecuencias de unas malas prácticas.

Martin Fowler es un importante y reconocido autor especialista en ingeniería del software y en el proceso de desarrollo (particularmente en el campo de la refactorización) el cual ya he tenido la oportunidad de citar en un artículo sobre una regla de CheckStyle que no comprendía su significado.

Steve C. McConnell es otro reputadísimo autor y especialista en ingeniería del software, tanto es así, que durante años fue considerado junto a Linus Torvalds y Bill Gates unas de las personas más influyentes en el negocio del desarrollo de software. En la actualidad posee una empresa llamada Construx Software que se dedica a consultoría y formación sobre buenas prácticas en desarrollo de software.

Ambos autores son responsables de las siguientes citas relacionadas con la comprensión del código:

Martin Fowler (traducción libre): «Cualquiera puede escribir código que un ordenador pueda entender. Los buenos programadores son aquellos que escriben código que los humanos puedan entender».

Steve McConnell (traducción libre): «El buen código es su mejor documentación. Cuando vayas a escribir un comentario, pregúntate, ¿cómo puedo mejorar el código para que este comentario no sea necesario?».

Sobre este tema ya publiqué una cita que se atribuye a Martin Golding en unos casos y a John F. Woods en otros que complementa perfectamente a éstas.

Como he comentado en otras ocasiones, hacer las cosas bien, tarde o temprano, marca la diferencia.

Comentó Bill Gates que: «Medir los avances de un programa utilizando como base el número de líneas de código es como medir el avance en la construcción de un avión por el peso». No puedo estar más de acuerdo.

El número de líneas de código no quiere decir nada más que ofrecer una medida del tamaño del software (una vez que esté listo) e indirectamente de su complejidad (ya que cuanto mayor sea probablemente mayor será la complejidad ciclomática y más complicado de mantener será la aplicación).

En muchas ocasiones los gestores de proyecto caemos en el reverso tenebroso de las líneas de código ya que es una medida tremendamente fácil de obtener y si en un mes se han desarrollado, pongamos por ejemplo, tres mil líneas de código más en el programa y en el mes siguiente se han codificado quinientas se podría llegar a conclusiones erróneas como por ejemplo, que en el mes anterior se ha sido más productivo y que en el siguiente se ha sido menos y en consecuencia que se ha avanzado menos que las cuatro semanas anteriores.

Todos sabemos que esto no tiene por qué ser necesariamente así, ya que no existe una relación directa entre el número de líneas de código y el grado de terminación del proyecto, es decir, podremos tirar muchas líneas de código y con ellas construir el armazón del proyecto, pero todavía quedaría lo más complejo que es perfilar el negocio y esas líneas de código no tienen la misma dificultad que las anteriores.

Estimar correctamente el avance de los proyectos no es ninguna tontería, porque si no se hace con un mínimo de criterio se darán estimaciones erróneas que darán lugar a que cuando se detecte que el proyecto vaya mal sea demasiado tarde para tomar medias efectivas. Además de que cada gestor de proyecto sea lo suficientemente objetivo en las mediciones resulta fundamental que en el ámbito de una organización exista un proceso uniforme para cuantificar el avance de los proyectos, no debiendo aplicar cada responsable el método o mecanismo que crea más adecuado sino aplicar uno común, ya que de lo contrario no habrá manera de confrontar y poner en común el conjunto de estimaciones que se reciban, lo que hará que la aplicación de soluciones sea menos efectiva y objetiva.

Otra de las métricas de Chidamber y Kemerer es WMC. El cálculo para una clase es bastante sencillo ya que es el resultado de la suma de la complejidad de cada método. ¿Qué valor se coge para la complejidad? Pues depende de la convención o implementación que se utilice en cada caso, en unos casos se utiliza como base la complejidad ciclomática, en otros, se asigna el valor 1 como complejidad a cada método, lo que hace que coincida la métrica con el número de métodos de la clase.

Esta métrica además de darnos la complejidad de una clase desde otro punto de vista diferente a las otras propuestas por Chidamber y Kemerer, puede servir de referencia para determinar la capacidad de reutilización de la misma, ya que teóricamente clases con más métodos son más específicas y por tanto de menos utilidad de cara a la reutilización.

En cualquier caso, tanto complejidad general de la clase como su capacidad de reutilización depende sobre todo de las características de los objetos que se quieren implementar con la clase y de la buena codificación que se aplique, ya que también podemos encontrarnos con clases con uno o dos métodos kilométricos que además de resultar bastante complejos de mantener, propicien que la clase no sea útil desde el punto de vista de la reutilización.

Por lo comentado anteriormente, particularmente me parece más acertado ponderar la complejidad de los métodos de una clase por su complejidad ciclomática (o utilizar cualquier otra estrategia para medir la complejidad, como por ejemplo su nivel de acoplamiento con métodos o atributos de otras clases) que darles a todos el mismo peso. La utilización de la complejidad ciclomática tiene también sus desventajas, ya que hace que la métrica real sea esa (la complejidad ciclomática) y no se tenga en cuenta a la hora de realizar la medición el número de métodos real de la clase (cuando puede ser una métrica muy sencilla para analizar el diseño de una clase), es decir, pueden ser métricas complementarias, perdiendo esa complementariedad en el momento en que todo se convierte en complejidad ciclomática.

Que una aplicación tenga una cobertura de pruebas unitarias alta (y se haya verificado el resultado de las mismas) supone una garantía para la reducción del número de errores de la aplicación tanto en la fase de pruebas funcionales como una vez que la misma se encuentre en producción.

La implementación de pruebas unitarias tiene su coste en tiempo, por lo que salvo que la organización para la que se desarrolla tenga unos umbrales de cobertura de código que haya que cumplir de manera obligatoria, es recomendable determinar en qué clases centramos nuestras pruebas unitarias, para que con el menor esfuerzo posible consigamos cubrir aquellas que pueden resultar más conflictivas y de esta forma, tal y como se comentó en el párrafo anterior reducir el número de errores que se propagan a etapas posteriores del desarrollo.

Estrategias para colocar las pruebas unitarias puede hacer muchas. Una de ellas es la que voy a comentar en este artículo y consiste en seleccionar aquellas clases que presentan de manera combinada una mayor complejidad ciclomática (posibles caminos lógicos) y mayor RFC (acoplamiento con métodos de otras clases).

Lo de seleccionar aquellas con mayor complejidad ciclomática puede parecer lógico, pero, ¿por qué combinarlas con las que tienen un mayor RFC? Pues porque el RFC es otra medida de complejidad y mide otro tipo de aspecto complejo, ya que se centra en la relación de la clase con otras y por tanto se puede ver afectada por cambios en cualquiera de los métodos de las clases con las que se relaciona.