Aplicación de un Proceso de Refactoring
guiado por Escenarios de Modificabilidad y
Code Smells
Trabajo final entregado para el grado de
Ingeniería en Sistemas
en la Facultad de Ciencias Exactas
Por
Manuel Alonso
Facundo H. Klaver
Bajo la supervisión de
Dr. Andrés Díaz-Pace
Dr. Santiago Vidal
Universidad Nacional del Centro de la Provincia de Buenos Aires
A mis padres, Claudia y Daniel. A mi hijo Fermín y su mamá.
MA
A mis padres, Susana y Daniel. A mi madrina Alicia. A mi compañera de la vida Eliana.
FHK
También queremos agradecer a nuestros directores, Andrés y Santiago, que nos ayudaron en todo lo posible para sacar adelante este trabajo.
Indice de Contenidos
1.Introducción...7
1.1.Modificabilidad como objetivo de calidad...7
1.2.Proceso de refactoring...8
1.3.Caso de estudio...9
1.4.Esquema general...10
2.Marco teórico...11
2.1.Evolución de los sistemas de software...11
2.2.Refactoring...12
2.2.1.Beneficios de refactorizar...13
2.2.1.1.Mejora el Diseño de Software...13
2.2.1.2.Hace al software fácil de entender...13
2.2.1.3.Ayuda a encontrar errores...13
2.2.2.Momentos para refactorizar...14
2.2.2.1.Al agregar una funcionalidad...14
2.2.2.2.Cuando se necesita corregir un error...14
2.2.2.3.Al revisar el código...15
2.2.3.Refactoring y diseño...15
2.2.3.1.Refactoring como una alternativa de diseño...15
2.2.3.2.Cambio de énfasis...15
2.2.3.3.Flexible...16
2.3.Calidad en el desarrollo de software...16
2.3.1.Atributos de calidad...16
2.3.2.Funcionalidad y arquitectura...17
2.3.3.Modificabilidad...18
2.3.3.1.Artefacto a modificar...18
2.3.3.2.Ambiente en que se realiza la modificación...18
2.3.4.Escenarios de Modificabilidad...19
2.3.5.Tácticas de modificabilidad...21
2.3.5.1.Localización de modificaciones...22
2.3.5.2.Prevención de efecto dominó...22
2.3.5.3.Retraso de tiempo de Binding...22
2.3.6.Deuda Técnica...22
2.3.6.1.Retrospectiva de deuda técnica...23
2.4.Métricas y code smells...24
2.4.1.Code Smells...25
2.4.1.1.Desarmonías de identidad...26
2.4.1.2.Desarmonías de Colaboración...28
2.5.Testing... 29
2.6.Resumen... 30
3.Trabajos Relacionados...32
3.1.Aspectos generales del refactoring de software...32
3.1.1.Formalización de los procesos de refactoring...32
3.1.2.Desafíos y beneficios de refactorizar...32
3.1.3.Dos tácticas de refactoring...34
3.1.4.Herramientas para cada táctica de refactoring...34
3.1.5.Refactoring guiado por métricas...34
3.2.Enfoques de refactoring...35
3.2.1.Identificación de oportunidades de refactoring...35
3.2.2.Refactoring basado en herramientas de visualización...35
3.2.3.Refactoring orientado por features...36
3.2.4.Modelo matemático para refactoring...37
3.3.Impacto de refactorings en la calidad del software...37
3.3.1.Efectos del refactoring en la calidad del software...37
3.3.2.Acoplamiento y cohesión como indicadores de buena calidad...38
3.3.3.Relación entre calidad y refactoring en casos de estudio...38
3.3.4.Modificabilidad de arquitecturas de software...38
3.3.5.Impacto de técnicas de refactoring en las métricas de calidad...39
3.4.Refactorings guiados por code smells...39
3.4.1.Evaluación de sistemas a partir del análisis objetivo y el análisis subjetivo...40
3.4.2.Detección automatizada de code smells...40
3.4.3.Identificación de Architectural Smells...40
3.4.4.Detección de code smells relevantes para la arquitectura...41
3.4.5.Orden apropiado de resolución de code smells...41
3.4.6.Una experiencia con code smells en Extreme Programming (XP)...42
3.5.Resumen... 43
4.Proceso de Refactorización...44
4.1.Definición del Proceso...44
4.1.1.Refactoring iterativo por etapas...45
4.1.1.1.Etapa 1: Análisis e identificación del problema ¿Qué refactorizar?...47
4.1.1.2.Etapa 2: Planteo de solución y definición de escenarios...49
4.1.1.3.Etapa 3: Priorización de backlog. ¿Por dónde conviene empezar?...50
4.1.1.4.Etapa 4: Implementación de escenario. ¡Refactorizar!...51
4.1.1.5.Etapa 5: Medición de resultados. ¿Es válida la refactorización?...53
4.1.1.6.Etapa 6: Evaluación. ¿Seguir refactorizando?...54
5.Caso de Estudio...55
5.1.SocialGraph, una herramienta para analizar redes sociales...55
5.2.Definición del sistema...56
5.2.1.Versiones de SocialGraph...56
5.2.2.Arquitectura actual de SocialGraph...56
5.2.3.Funcionalidades de SocialGraph...60
5.2.4.Requerimientos de SocialGraph...60
5.3.Aplicación del proceso de refactorización a SocialGraph...61
5.4.Iteración 0: Preliminar...61
5.4.1.Análisis general del problema...61
5.4.1.1.Anomalía 1: Estructura del código del proyecto...62
5.4.1.2.Anomalia 2: Acoplamiento entre capas...62
5.4.1.3.Anomalía 3: Diseño ligado a correos electrónicos...62
5.4.2.Priorización de las anomalías...62
5.4.3.Cobertura de test de la aplicación...64
5.4.4.Observaciones de la Iteración 0...65
5.5.Iteración 1: Re-estructuración del Proyecto...65
5.5.1.Etapa 1: Análisis e identificación del problema...65
5.5.1.1.Documentación del proyecto...65
5.5.1.2.Detección de code smells...65
5.5.1.3.Identificación de los problemas en el diseño...66
5.5.2.Etapa 2: Planteo de solución y definición de escenarios...67
5.5.2.1.Definir refactorings a realizar...67
5.5.2.2.Especificar Escenarios de Modificabilidad...67
5.5.2.2.1.Escenario 1.1. Mavenizar Proyecto...67
5.5.2.2.2.Escenario 1.2. Reestructurar dependencia con aplicación WordCram...68
5.5.2.2.3.Escenario 1.3. Reestructurar dependencia con aplicación GATE...69
5.5.2.3.Agregar al backlog...69
5.5.3.Etapa 3: Priorización de backlog...69
5.5.3.1.Nivel de Impacto...70
5.5.3.2.Nivel de Esfuerzo...70
5.5.3.3.Ubicación en cuadrantes...70
5.5.4.Etapa 4: Implementar Escenarios...71
5.5.4.1.Escenario 1.1: Mavenizar proyecto...71
5.5.4.1.1.Verificar cobertura de test...71
5.5.4.1.2.Implementar Escenario...71
5.5.4.1.3.Ejecutar casos de test...75
5.5.4.2.Escenario 1.2: Reestructurar dependencia con aplicación WordCram Proyecto...76
5.5.4.2.1.Verificar cobertura de test...76
5.5.4.2.2.Implementar Escenario...76
5.5.4.2.3.Ejecutar casos de test...76
5.5.4.3.Escenario 1.3. Reestructurar dependencia con aplicación GATE...76
5.5.4.3.1.Verificar cobertura de test...76
5.5.4.3.2.Implementar Escenario...77
5.5.4.3.4.Notas sobre la implementación de los escenarios...78
5.5.5.Etapa 5: Medición de Resultados...79
5.5.5.1.Análisis basado en métricas y documentación...79
5.5.5.2.Análisis subjetivo y valoración del grupo de desarrollo...79
5.5.5.3.Validación de escenarios...80
5.5.6.Etapa 6: Etapa 6: Evaluación...80
5.6.Iteración 2: Diseño ligado a correos electrónicos...80
5.6.1.Etapa 1: Análisis e identificación del problema...80
5.6.1.1.Documentación del proyecto...80
5.6.1.2.Detección de code smells...82
5.6.1.3.Identificación de los problemas en el diseño...83
5.6.2.Etapa 2: Planteo de solución y definición de escenarios...83
5.6.2.1.Definir refactorings a realizar...83
5.6.2.2.Especificar Escenarios de Modificabilidad...83
5.6.2.2.1.Escenario 3.1: Abstraer el modelo para la construcción del grafo...83
5.6.2.2.2.Escenario 3.1.1: Aplicar patrón Strategy a la clase GraphBuilder...84
5.6.2.2.3.Escenario 3.1.2: Abstraer la clase CommunicationEdge de la clase Mail en el modelo...85
5.6.2.2.4.Escenario 3.1.3: Abstraer la clase PersonVertex de la clase Mail en el modelo...85
5.6.2.2.5.Escenario 3.1.4: Abstraer la clase FileVertex y AttachmentEdge de la clase Mail en el modelo. ... 86
5.6.2.2.6.Escenario 3.1.5: Abstraer la clase TagFinder de la clase Mail en el modelo...87
5.6.2.2.7.Escenario 3.1.6: Refactorizar God Class en GraphPersistenceHelper...88
5.6.2.2.8.Escenario 3.2: Abstraer la clase Mail en la interfaz gráfica de la herramienta...88
5.6.2.2.9.Escenario 3.3: Refactorizar sección de código de “importación de datos”...89
5.6.2.3.Agregar al backlog...90
5.6.3.Etapa 3: Priorización de backlog...90
5.6.3.1.Nivel de Impacto...90
5.6.3.2.Nivel de Esfuerzo...91
5.6.3.3.Ubicación en cuadrantes...91
5.6.4.Etapa 4: Implementación de escenarios...92
5.6.4.1.Escenario 3.1.1: Aplicar patrón Strategy a la clase GraphBuilder...92
5.6.4.1.1.Verificar cobertura de test...92
5.6.4.1.2.Implementar Escenario...92
5.6.4.1.3.Ejecutar casos de test...94
5.6.4.2.Escenario 3.1.2: Abstraer la clase CommunicationEdge de la clase Mail en el modelo...94
5.6.4.2.1.Verificar cobertura de test...94
5.6.4.2.2.Implementar Escenario...94
5.6.4.2.3.Ejecutar casos de test...96
5.6.4.3.Escenarios 3.1.3, 3.1.4 y 3.1.5...96
5.6.4.4.Escenario 3.1.6: Refactorizar God Class en GraphPersistenceHelper (GPH)...96
5.6.4.4.1.Verificar cobertura de test...96
5.6.4.4.2.Implementar Escenario...96
5.6.4.5.Escenario 3.2: Abstraer la clase Mail en la interfaz gráfica de la herramienta...98
5.6.4.5.1.Verificar cobertura de test...98
5.6.4.5.2.Implementar Escenario...98
5.6.4.5.3.Ejecutar casos de test...99
5.6.4.6.Escenario 3.3: Refactorizar sección de código de “importación de datos”...99
5.6.4.6.1.Verificar cobertura de test...99
5.6.4.6.2.Implementar Escenario...99
5.6.4.6.3.Ejecutar casos de test...103
5.6.5.Etapa 5: Medición de resultados...103
5.6.5.1.Análisis basado en métricas y documentación...103
5.6.5.2.Análisis subjetivo y valoración del grupo de desarrollo...104
5.6.5.3.Validación de escenarios...105
5.6.6.Etapa 6: Evaluación...105
6.Conclusiones...106
6.1.Evaluación de la solución...106
6.2.Aportes y beneficios...107
6.3.Limitaciones...108
6.4.Trabajos futuros...109
1. Introducción
Una característica intrínseca de los sistemas de software es su necesidad de evolucionar. A medida que el software se ha mejorado, modificado y adaptado a las nuevas exigencias, el código se vuelve más complejo y se aleja de su diseño original, lo que reduce la calidad del software [Mens2004]. El objetivo de este trabajo es brindar un enfoque de refactorización que garantice una mejora en la modificabilidad del software.
1.1. Modificabilidad como objetivo de calidad
Los sistemas de software son construcciones complejas de ingeniería [Lanza2007]. Un sistema de software moderno es desarrollado por muchas personas al mismo tiempo, y esto puede dar lugar a problemas de comunicación y problemas de complejidad. Los proyectos de desarrollo de software evolucionan en el tiempo y necesitan mantenimiento constante. Un cambio en una parte del sistema puede afectar otras partes del mismo. Por eso, establecer el diseño adecuado de un sistema es la clave para que sea más entendible y soporte fácilmente cambios en el futuro. Para mantener un equilibrio en el sistema es necesario una mejora continua de la calidad del diseño de dicho sistema.
Los atributos de calidad son características que permiten verificar y medir el grado de satisfacción de los usuarios y/o diseñadores con respecto al sistema de software [ISO9126]. Consideraciones de la lógica de negocio determinan qué cualidades deben ser tenidas en cuenta en la arquitectura del sistema. Estas cualidades están por encima de la funcionalidad, que es la declaración básica de las capacidades, los servicios y el comportamiento del sistema [Bass2003].
El desarrollo y mantenimiento de un sistema de software, sobre todo el caso de proyectos grandes, requieren de un proceso ordenado, guiado por atributos de calidad, definidos y priorizados de manera que garanticen tanto la satisfacción de los stakeholders como la sustentabilidad del proyecto. Si el proceso de desarrollo es guiado solamente por requerimientos funcionales, a fin de cumplir con los features solicitados por el cliente, no siempre se garantiza un nivel de calidad que permita la escalabilidad del proyecto en términos de performance, trazabilidad, seguridad, modificabilidad, etc. Esta problemática es común en muchos sistemas de software.
Con la evolución del sistema y la introducción de nuevos features, se suelen tomar decisiones de diseño que no van en concordancia con la idea original. Asi, se incurre en un desvío en la arquitectura que puede traducirse en un deterioro de la modificabilidad del sistema.
modificabilidad. De esta manera, un arquitecto puede especificar en un lenguaje común a todos los desarrolladores, las tareas a realizar y los resultados a obtener.
Los resultados de la implementación de estos escenarios deben ser validados, y preferentemente, la mejora de calidad del sistema debe poder medirse. Durante el ciclo de desarrollo del sistema es importante contar con métricas que brinden una noción de la evolución de la calidad a través del tiempo. Las métricas miden elementos estructurales y, como tales, pueden revelar problemas ocultos. En este trabajo se utilizó una serie de code smells que permiten detectar, a nivel de implementación y diseño, síntomas de baja modificabilidad. Sin embargo, siempre habrá una brecha entre los síntomas detectados y la evaluación profunda que un experto en diseño puede hacer en base a esos síntomas [Fowler1999]. Por lo tanto, es importante combinar y complementar estos indicadores con el análisis subjetivo y cualitativo que aportan los desarrolladores del proyecto y responsables del refactoring.
1.2. Proceso de refactoring
Refactoring es el proceso de cambiar un sistema de software de manera tal que se no altere su comportamiento externo, pero se mejore su estructura interna [Fowler1999]. Bajo esta definición, cualquier cambio, por pequeño sea, si representa una mejora y no afecta la funcionalidad del sistema, podría considerarse una “refactorización”.
El objetivo en este trabajo es ir más allá de esas pequeñas mejoras que a diario realizan los programadores para pensar en la refactorización como un proceso enmarcado en el desarrollo de software que permita mejorar la calidad del sistema a partir de un incremento de su modificabilidad. Esto significa que las refactorizaciones son realizadas con un enfoque ordenado, guiado por objetivos concretos, que lleva a modificaciones definidas y validadas, con el testeo de las áreas de código afectadas y el análisis de los resultados.
En este contexto, se utilizó un proceso de refactorización en el cual se pueden identificar un conjunto de etapas iterables de manera incremental, que conforman un proceso guiado por escenarios de modificabilidad y code smells. Las etapas se pueden clasificar de la siguiente manera:
• Etapas de Análisis: al comienzo del proceso es necesario el análisis del código del sistema para detectar errores y síntomas de problemas asociados al desarrollo y las malas prácticas de diseño.
• Etapas de Implementación: con el diagnóstico del sistema, se pasa a la proposición y realización de modificaciones con el objetivo de solucionar esos problemas detectados.
En la Figura 1.1 se puede ver un resumen del proceso definido en este trabajo.
Figura 1.1. Proceso de refactorización resumido.
1.3. Caso de estudio
ha visto estancado por la erosión del diseño original y la falta de documentación durante el proceso de desarrollo.
Estos nuevos requerimientos ponen en evidencia la falta de planificación del sistema, su deuda técnica y un bajo nivel de modificabilidad. Es por esto que utilizó SocialGraph como caso de estudio y se instanció el proceso de refactorización en el desarrollo de esta herramienta. Así, se logró una mejora cualitativa en términos de modificabilidad, que permite desarrollar la funcionalidad requerida y mejorar considerablemente la arquitectura del sistema.
1.4. Esquema general
Este trabajo final está organizado de la siguiente manera:
En el capítulo 2 se introducen los conceptos tomados como referencia para este trabajo. Se sientan las bases de cómo evolucionan los sistemas de software, cuáles son las implicancias de realizar un refactoring, el análisis a través de code smells y la especificación de los escenarios de calidad.
En el capítulo 3 se describen los trabajos y experiencias relacionados con aspectos generales del refactoring de software, diferentes enfoques de refactoring, refactorings guiados por code smells. Estos enfoques son estudiados y evaluados con el fin de contextualizar el presente trabajo.
En el capítulo 4 se describe un proceso de refactoring centrado en la mejora de calidad guiado por escenarios de modificabilidad y code smells. Se detallan las etapas propuestas en el proceso y las actividades correspondientes a cada una de ellas.
En el capítulo 5 se pone a prueba el proceso detallado en el capítulo anterior, tomando como caso de estudio la aplicación SocialGraph. A partir del estado actual del caso de estudio y sus objetivos de desarrollo, se instancia el proceso de refactorización con sus etapas y actividades.
2. Marco teórico
En este capítulo se introducen los conceptos básicos de la evolución de sistemas de software y los problemas que esto trae con el paso del tiempo y la introducción de nuevos features. Durante el desarrollo del sistema se suelen realizar cambios en la arquitectura que no van en concordancia con su idea original. De esta manera, se cae en un deterioro de la calidad a partir de la pérdida de modificabilidad, mantenibilidad, flexibilidad y testeabilidad, entre otros atributos. La implementación de refactorings aparece como una herramienta de desarrollo que puede ayudar a mejorar la calidad sin alterar el comportamiento observable del sistema. A partir de la experiencia de expertos en la materia, se introducen las principales características de los refactorings, sus ventajas y desventajas, el impacto en el código y el diseño del sistema y la adopción del refactoring como una práctica regular en el proceso de desarrollo.
El principal objetivo del refactoring es mejorar la calidad del sistema, en particular, su modificabilidad. Por lo tanto, se introducen los conceptos de atributos de calidad, calidad de la arquitectura, deuda técnica, tácticas de modificabilidad y escenarios de modificabilidad, que son utilizados en el proceso de refactorización definido en este trabajo.
Para refactorizar, es importante apoyarse en métricas que permitan evaluar la calidad del diseño. En este trabajo las principales métricas utilizadas son los code smells: un indicador de síntomas de malas prácticas a nivel de código y diseño. Se presenta una clasificación y las características de los principales code smells.
La implementación de los cambios a realizar en el refactoring no debe afectar a la funcionalidad del sistema. Esto trae aparejado ciertos riesgos que deben mitigarse a través del testeo sistemático del sistema. Al final de este capítulo se presentan algunos lineamientos para realizar el testeo y la validación de que el refactoring se realizó sin modificaciones en la funcionalidad observable del sistema.
Todos estos conceptos son utilizados durante el desarrollo de este trabajo para definir y validar un proceso de refactoring guiado por escenarios de calidad y code smells.
2.1. Evolución de los sistemas de software
A medida que el software se va mejorando, modificando y adaptando a las nuevas exigencias, el código se vuelve más complejo y se aleja de su diseño original, lo que reduce la calidad del software [Mens2004].
Los requerimientos evolucionan y es por eso que el sistema tiene que hacer lo propio para no quedar obsoleto [Lehman2001]. Lehman en tres de sus leyes hace referencia a esta problemática relacionada con el mantenimiento del software:
● Decremento de la calidad: la calidad de los sistemas software comenzará a disminuir a menos que dichos sistemas se adapten a los cambios de su entorno de funcionamiento.
● Cambio continuo: un sistema que se utiliza en un ambiente del mundo real debe cambiar o progresivamente será menos útil en ese ambiente.
● Complejidad creciente: a medida que un sistema evoluciona, su estructura se vuelve más compleja y se necesita de recursos adicionales para preservar y simplificar su estructura.
Para que los sistemas de software evolucionen en el tiempo sin que su calidad sufra un deterioro se deben realizar cambios progresivos y continuos para que el mismo se adapte a los nuevos requerimientos y al paso del tiempo. Si las sucesivas modificaciones no son efectuadas correctamente, el código se vuelve difícil de mantener, se pueden introducir nuevos errores y se vuelve más difícil adaptar el sistema a los nuevos requerimientos.
Dados los tiempos de desarrollo actuales es importante tener un análisis de cuáles son las áreas del sistema que deben mejorar su calidad y cuales están estables o no necesitan cambios según los objetivos a futuro del sistema. Esto permitirá priorizar los cambios a realizar y lograr una planificación más certera de los tiempos que demandaran estos cambios.
2.2. Refactoring
Refactoring es el proceso de cambio de un sistema de software de manera tal que no altera el comportamiento externo del código pero mejora su estructura interna. Es una manera disciplinada de limpiar código que minimiza las posibilidades de introducir errores. En esencia cuando se refactoriza se está mejorando el diseño del código después de que ha sido escrito [Fowler1999].
El propósito de la refactorización es hacer el software más fácil de entender y modificar. Sólo los cambios realizados para hacer el software más fácil de entender son refactorizaciones. Un software fácil de entender es un software fácil de modificar [Fowler1999]. Un buen contraste es la optimización del rendimiento. Al igual que la refactorización, la optimización del rendimiento no suele cambiar el comportamiento de un componente (que no sea su velocidad); sólo altera la estructura interna. Sin embargo, el propósito es diferente. La optimización del rendimiento a menudo hace que el código más difícil de entender, pero hay que hacerlo para obtener el rendimiento que se necesita.
2.2.1. Beneficios de refactorizar
Al refactorizar un sistema no se pueden solucionar todos los problemas del mismo. Sin embargo, es una herramienta valiosa, si se aplica regularmente. El refactoring es una herramienta que puede, y debe, ser utilizada para diversos fines. Las motivaciones para realizar un refactoring pueden ser varias. A continuación se discuten algunas de ellas.
2.2.1.1. Mejora el Diseño de Software
Sin refactorización, el diseño del programa decaerá. Los programadores desarrollan nuevos requerimientos con la finalidad de alcanzar objetivos a corto plazo o cambios en el sistema realizados sin una comprensión completa del diseño del sistema. Este pierde su estructura con el transcurso del tiempo y se hace más difícil ver el diseño mediante la lectura del código.
El refactoring es en parte poner orden en el código. La pérdida de la estructura de un sistema tiene un efecto acumulativo. Cuanto más difícil es ver el diseño en el código del sistema, más difícil es mantenerlo. Un código mal diseñado generalmente toma más líneas para que funcione. Reducir la cantidad de código no hará que el sistema funcione más rápido, sin embargo, puede hacer una gran diferencia en la modificabilidad del código. Refactorizar regularmente ayuda a conservar la estructura del sistema.
2.2.1.2. Hace al software fácil de entender
El programador escribe código que indica a la computadora qué hacer, y esta, responde haciendo exactamente lo que le dice. Hay una brecha entre lo que el programador quiere que haga y lo que el código realmente hace [Fowler1999].
A la hora de programar, es una mala práctica que no se piense en que otro programador o el mismo en un futuro tenga que modificar el código. Este es un problema que impacta cuando un programador necesita una semana para hacer un cambio que habría tomado sólo una hora si el código fuera más entendible.
El refactoring ayuda a hacer el código más legible. Cuando existe código que funciona, pero no está muy bien estructurado, un poco de tiempo dedicado a refactorizar puede hacer que sea más entendible y rápido de modificar.
Se pueden empezar realizando pequeños refactorings corrigiendo detalles. Como el código va quedando más claro, se pueden ver aspectos del diseño que no se podían ver antes.
2.2.1.3. Ayuda a
encontrar
errores
2.2.2. Momentos para refactorizar
Se recomienda refactorizar todo el tiempo en pequeñas ráfagas. El desarrollador no decide refactorizar, refactoriza porque quiere hacer algo más, y refactorizando ayuda a preparar el sistema para el cambio. A continuación se discuten algunas guías de cuándo es recomendable refactorizar.
2.2.2.1. Al agregar una funcionalidad
El momento más común para refactorizar es cuando se quiere agregar una nueva funcionalidad a un sistema. El código puede haber sido escrito por el mismo programador al que le toca implementarla o por otro. Al momento de agregarla, el programador ve que si se hubiera diseñado el código de otra manera, la implementación sería más fácil. Se debe refactorizar el diseño para que el sistema se más fácil de modificar en el futuro y por la tanto el proceso de agregar una nueva funcionalidad sea más rápido.
Figura 2.1. Ejemplo de refactorización.
Supongamos el caso de un sistema en cual se realiza la construcción de un grafo a partir de una base de datos de correos electrónicos (Mails). Esta funcionalidad se encuentra en la clase
GraphBuilder como se puede ver en la figura 2.1a. A futuro se requiere que el sistema pueda
construir grafos tanto a partir de Mails como de otras fuentes de información. Es momento de refactorizar, se utiliza el patrón strategy para refactorizar el diseño del sistema, como se puede ver en la figura 2.1b. Con esta refactorización, el sistema ya está preparado para incorporar de forma sencilla y rápida la nueva funcionalidad requerida. En la figura 2.2 se aprecia el diseño luego de la incorporación de una estrategia para construir el grafo a partir de mensajes de chats (GraphBuildingStrategyChat).
2.2.2.2. Cuando se necesita corregir un error
2.2.2.3. Al revisar el código
Las revisiones de código ayudan a difundir el conocimiento a través de un equipo de desarrollo. Los desarrolladores más experimentados pasan conocimiento a otros con menos experiencia. También son muy importantes para la escritura de código claro. El código puede parecer claro para el desarrollador que lo escribió, pero no para el resto del equipo. Las revisiones también dan la oportunidad para que más personas sugieren ideas útiles.
Esta idea de la revisión de código activo es llevada a su límite con la Programación Extrema (XP) y su práctica de la programación en parejas [Beck2000]. Con esta técnica todo el desarrollo importante se hace con dos programadores en una sola máquina. En efecto se trata de una revisión de código continua e integrada en el proceso de desarrollo, y la refactorización que tiene lugar se integra también.
2.2.3. Refactoring y diseño
El refactoring tiene un papel especial como complemento del diseño. Lo más probable en desarrollos donde no se dedica tiempo a pensar en el diseño del sistema antes de implementar, es recaer en la re-implementación de funcionalidades que puede ser un costo considerable para el proyecto.
Muchas personas consideran que el diseño debe ser la pieza clave de un proyecto y la programación sólo algo mecánico. La analogía en la que se cae con esta afirmación es la del diseño de un plano de ingeniería y la programación vendría a ser el trabajo de construcción. Pero el software es muy diferente de una construcción de un edificio, ya que es mucho más maleable.
2.2.3.1. Refactoring como una alternativa de diseño
Un argumento utilizado es que la refactorización puede ser una alternativa al diseño inicial. En este escenario no se hace ningún diseño en absoluto. Se codifica la primera solución que le viene al programador a la cabeza, y al conseguir que funcione, refactorizar el código hasta que quede mejor estructurado. Este enfoque puede funcionar y se le ve mucho en los desarrollos con metodologías ágiles preferentemente XP. Sin embargo este enfoque no es la forma más eficiente de trabajar. Incluso los programadores extremos hacen algún diseño primero. Ellos evalúan diversas ideas con tarjetas CRC o similares hasta que tengan una primera solución plausible. Sólo después de la generación de un primera solución van a codificar y luego refactorizar. El punto es que la refactorización cambia el papel del diseño inicial. Si no se tiene pensado refactorizar, hay mucha presión para conseguir que el diseño inicial sea el correcto. La idea que acompaña a poner tanto énfasis en diseñar antes de codificar es que cualquier cambio que se deba realizar, cuanto mas tarde mas caro resultará. De este modo se pone más tiempo y esfuerzo en el diseño inicial para evitar la necesidad de tales cambios.
2.2.3.2. Cambio de énfasis
una solución razonable. A medida que se construye la solución, más se entiende sobre el problema, puede ser que la mejor solución sea distinta a la que originalmente se pensó. Con refactorización esto no es un problema, porque ya no es caro hacer los cambios.
2.2.3.3. Flexible
Las soluciones flexibles son complejas. El software resultante es más difícil de mantener, en general, a pesar de que es más fácil de cambiar la dirección que se tenía en un principio. En caso de que se quiera adaptar el sistema, se debe entender cómo cambiar el diseño. La construcción de flexibilidad en todo el sistema lo hace mucho más complejo y costoso de mantener. El tema en este aspecto es un trade-off entre el costo de mantener un sistema con una gran flexibilidad y el beneficio si estamos preparados para adaptarnos fácilmente a los cambios, lo difícil es saber cual es la medida justa.
Con la refactorización se tratan los riesgos del cambio de forma diferente. Todavía se piensa en los posibles cambios, todavía se consideran soluciones flexibles. Pero en lugar de aplicar estas soluciones flexibles, se deben realizar las siguientes preguntas, "¿Qué tan difícil va a ser refactorizar una solución sencilla a una solución flexible?" Si sucede como la mayoría de las veces, la respuesta es "bastante fácil", entonces se debe implementar la solución simple.
La refactorización puede conducir a diseños más simples sin sacrificar la flexibilidad. Esto hace que el proceso de diseño más fácil y menos estresante. Se apunta a construir la funcionalidad más simple con la que se es posible trabajar y refactorizar de ser necesario. En cuanto al diseño flexible, complejo, la mayoría de las veces no se va a necesitar.
2.3. Calidad en el desarrollo de software
2.3.1. Atributos de calidad
De acuerdo con el modelo ISO 9126 los atributos de calidad son características que permiten verificar y medir el grado de satisfacción de los usuarios y/o diseñadores con respecto al sistema de software [ISO9126].
Consideraciones de la lógica de negocio determinan cualidades que deben ser tenidas en cuenta en la arquitectura de un sistema. Estas cualidades están por encima de la funcionalidad, que es la declaración básica de las capacidades, los servicios y el comportamiento del sistema.
2.3.2. Funcionalidad y arquitectura
Los atributos de funcionalidad y los de calidad son ortogonales. Si no fuera así, la elección de la función dictaría el nivel de seguridad o el rendimiento o la disponibilidad o la facilidad de uso [Bass2003].
Claramente, es posible elegir independientemente un nivel deseado de cada uno. Ahora bien, esto no quiere decir que cualquier nivel de cualquier atributo de calidad se puede lograr con cualquier feature. El arquitecto toma diferentes opciones que determinarán el nivel relativo de la calidad, estas darán lugar a una mejora en un atributo de calidad y otras conducirán en otra dirección.
La importancia de los atributos de calidad debe ser considerada tanto en el diseño como en la implementación, aunque ningún atributo de calidad es totalmente dependiente de uno ni otro aspecto del sistema.
La funcionalidad es la capacidad del sistema para hacer el trabajo para el que fue diseñado. Una tarea requiere que muchos o la mayoría de los elementos del sistema trabajen de manera coordinada para completar el trabajo, tal como para construir una casa, albañiles, electricistas, plomeros, pintores y carpinteros todos trabajan juntos cooperativamente. Por lo tanto, si a los elementos no se le ha asignado las responsabilidades correctas o no han sido dotados con las cualidades para poder coordinarse con otros (de modo que, por ejemplo, no saben cuándo es el momento para que comienzan su parte de la tarea), el sistema será incapaz de ofrecer la funcionalidad requerida.
La funcionalidad puede lograrse mediante el uso de una combinación de estructuras posibles. De hecho, si la funcionalidad fuera el único requisito, el sistema podría existir como un único módulo monolítico con ninguna estructura interna en absoluto. En lugar de ello, se descompone en módulos para que sea comprensible y para apoyar una variedad de otros fines. De esta manera, la funcionalidad es en gran medida independiente de la estructura.
Obtener resultados satisfactorios en el desarrollo de un sistema es una cuestión de conseguir que el “big picture” de la arquitectura esté en concordancia con la implementación. Por ejemplo: la modificabilidad es determinada por cómo la funcionalidad es dividida (arquitectura) y por las técnicas de codificación dentro de un módulo (no arquitectural). Por lo tanto, la modificabilidad de un sistema de software es la facilidad con la que se pueden introducir cambios en el entorno, requisitos o especificación funcional.
Quedan claras dos cosas sobre esta sección:
1) La arquitectura es fundamental para la realización de muchas cualidades de interés en un sistema, y estas cualidades pueden diseñarse y ser evaluadas a nivel arquitectónico.
El atributo de calidad que guiará este proceso de refactoring es la modificabilidad, ya que el objetivo principal es atacar este atributo. De esta forma, con la mejora de la modificabilidad, será más fácil y rápido adaptar nuevas funcionalidades en el futuro.
Siguiendo el ejemplo descrito en la figura 2.1, se puede notar que el refactoring impactó de forma positiva en la modificabilidad del sistema. Es decir, al momento de implementar la construcción de grafos a partir de otra fuente de información (Chats), no se requiere cambiar el diseño.
Figura 2.2. Ejemplo de implementación de nueva funcionalidad sobre sistema refactorizado.
2.3.3. Modificabilidad
La modificabilidad tiene que ver con el costo del cambio. Esto trae a colación dos preocupaciones [Bass2003]:
2.3.3.1. Artefacto a modificar
Un cambio puede ocurrir en cualquier aspecto de un sistema, más comúnmente en la funcionalidad, el ambiente de ejecución, en la calidad de los artefactos que lo componen, su capacidad (número de usuarios soportados, número de operaciones simultáneas, etc.), como también en la interfaz de usuario.
2.3.3.2. Ambiente en que se realiza la modificación
Una vez que se ha determinado un cambio, la nueva aplicación debe ser diseñada, implementada, probada y desplegada. Todas estas acciones toman tiempo y dinero, los cuales se puede medir.
2.3.4. Escenarios de Modificabilidad
En un escenario de modificabilidad, una petición para una modificación llega (el estímulo) y los desarrolladores deben aplicar la modificación, sin efectos secundarios, para luego probar y desplegar la modificación [Bass2003].
Una colección de escenarios concretos se puede utilizar como requerimientos de atributos de calidad de un sistema. Cada escenario es lo suficientemente concreto para ser significativo tanto para el arquitecto como el programador. En la elicitación de requerimientos, normalmente se organiza la discusión de escenarios generales por atributos de calidad; si el mismo escenario es generado por dos atributos diferentes, uno puede ser eliminado.
En la tabla 2.1 se hace una descripción detallada de cada una de las partes de un escenario y los posibles valores que pueden adquirir.
Porción del Escenario
Descripción Valores Posibles
Fuente del Estímulo
Esta parte especifica quién hace los cambios: el desarrollador, administrador de sistemas o un usuario final. Claramente, debe haber mecanismos para permitir que el administrador del sistema o el usuario final puedan modificar un sistema, pero esto es una ocurrencia común.
Usuario final, desarrollador, administrador del sistema
Estímulo Esta parte específica los cambios a realizar. Un cambio puede ser la adición de una función, la modificación de una función existente, o la eliminación de una función. También se puede hacer para las cualidades del sistema por lo que es más sensible, aumentando su disponibilidad, y así sucesivamente. La capacidad del sistema también puede cambiar. Aumentar el número de usuarios simultáneos es un requisito frecuente.
Desea agregar / eliminar / modificar una funcionalidad, atributo de calidad, capacidad.
Artefacto Define qué se va a cambiar: la funcionalidad de un sistema, su plataforma, su interfaz de usuario, su medio ambiente, u otro sistema con el que interactúa.
Interfaz de usuario, plataforma, entorno, sistema que
interactúa con el sistema objetivo. Ambiente Especifica cuándo se puede realizar el cambio: en tiempo de diseño,
en tiempo de compilación, en tiempo de construcción, e tiempo de inicio, o en tiempo de ejecución.
Tiempo de ejecución, compilación, build, diseño,
probarlo y desplegarlo. la arquitectura a ser modificados, realiza modificaciones sin afectar otras funcionalidades, testea
modificaciones, despliega modificaciones. Medida de
la
Respuesta
Todas las posibles respuestas toman tiempo y cuestan dinero, tiempo y costo son las medidas más deseables. El tiempo no siempre es posible de predecir, sin embargo, se pueden pensar otras medidas que se utilizan con frecuencia, como la magnitud del cambio (número de módulos afectados). En relación con la modificabilidad, la respuesta también puede medirse a partir de indicadores
estadísticos del código y el diseño o bien partir del criterio de un experto.
Costo en términos de número de
elementos afectados, esfuerzo, dinero; medida en que esto afecta a otras funciones o atributos de calidad.
Tabla 2.1. Descripcion y valores posibles de las partes de un escenario de modificabilidad.
Volviendo al ejemplo del grafo y la refactorización de su construcción, este requerimiento se podría describir de la siguiente forma: “Se desea que el desarrollador implemente un cambio de modo tal que se puedan soportar otras fuentes de información en la construcción del grafo. se hará este cambio en el código en tiempo de diseño, se tardará menos de tres horas para hacer y probar el cambio, y no ocurrirán efectos secundarios en el comportamiento.”
Figura 2.3. Ejemplo gráfico y resumido de escenario.
Porción del Escenario
Valores
Fuente del Estímulo El cambio debe ser realizado por el desarrollador
Estímulo Poder soportar en un futuro varias fuentes de información
Artefacto Clase GraphBuilder
Ambiente Implementación.
Respuesta Realiza modificaciones sin afectar funcionalidades del sistema.
Medida de la Respuesta
Tiempo de realización: 3 horas/hombre. Mejora de la modificabilidad en el futuro
Tabla 2.2. Ejemplo de especificación de escenario de modificabilidad.
2.3.5. Tácticas de modificabilidad
Las tácticas de modificabilidad tienen como objetivo controlar el tiempo y costo de los cambios en la implementación, pruebas y puesta en ejecución [Bass2003]. Los distintos tipos de tácticas están organizadas acorde con sus metas específicas. En la figura 2.4 se aprecia un esquema resumido de estas tácticas. A continuación se describe su clasificación.
2.3.5.1. Localización de modificaciones
El principal objetivo de este conjunto de tácticas de modificabilidad es minimizar el número de módulos que necesitarán ser modificados en iteraciones posteriores desarrollo. Las siguientes tácticas de localización de modificaciones son empleadas durante la fase de diseño e implementación del proceso de desarrollo [Bass2003].
2.3.5.2. Prevención de efecto dominó
Se refiere a la posibilidad de introducir errores a módulos que no son afectados directamente por el cambio. Esta situación se da si es necesario modificar un componente de software pero existen otros componentes que dependen de este. Este tipo de tácticas intenta minimizar el número de módulos que necesitarán ser modificados por consecuencia de modificar el primer componente [Bass2003].
2.3.5.3. Retraso de tiempo de Binding
Este tipo de tácticas de modificabilidad tienen como objetivo retrasar el momento en que se realiza el enlace entre componentes, es decir la configuración del sistema es especificada hasta el momento de su ejecución. Con el uso de esta táctica es posible que los desarrolladores puedan modificar o sustituir componentes durante el proceso de desarrollo para la realización de pruebas de los componentes o de módulos del sistema. Algunas de las tácticas más utilizadas son [Bass2003].
2.3.6. Deuda Técnica
El término “technical debt” (deuda técnica) fue introducido en 1992 por Ward Cunningham. Es una metáfora que viene a explicar que la falta de calidad en el código fuente del proyecto, genera una deuda que repercutirá en intereses, tanto en el mantenimiento de un software, como en la propia operativa funcional de la aplicación.
En esta metáfora, hacer las cosas de la manera rápida y no planeada acarrea una deuda técnica, que es similar a una deuda financiera. Al igual que una deuda financiera, la deuda técnica incurre en el pago de intereses, que vienen en la forma de un esfuerzo extra que habrá que hacer en el futuro desarrollo. Se podrá optar por seguir pagando los intereses, o bien resolver el problema de fondo, refactorizando el diseño. Aunque es costoso “pagar” (resolver) el problema de fondo, se gana con pagos reducido de “intereses” en el futuro.
La gestión de la deuda técnica debe reservarse para los casos en los que se ha adoptado una estrategia de diseño que no es sostenible en el largo plazo, pero se obtiene un beneficio a corto plazo, tal como lanzar una release. El punto es que los rendimientos de la deuda se vean pronto, pero la misma tiene que ser pagada tan pronto como sea posible [Fowler2009].
cuánto la recompensa por una versión anterior es mayor que los costos de pagarla. Un equipo que ignora las prácticas de diseño está asumiendo su deuda imprudente sin siquiera darse cuenta de en cuánto “interés” se está incurriendo.
Un problema con el uso de la metáfora de la deuda es que no se puede concebir un paralelo con la toma de una deuda financiera prudente inadvertida. En el ambiente financiero sería difícil de explicar a los directivos de un banco por qué esta deuda apareció. El tema es que en un desarrollo de software esta clase de deuda es inevitable y por lo tanto se debe esperar. Incluso los mejores equipos tendrán que lidiar con la deuda [Fowler2009].
Pero no siempre podemos adelantarnos y prevenir la deuda, por lo tanto, en estos casos sólo se puede recurrir a un proceso de refactoring. Si se quiere que un proyecto llegue a buen puerto, al final siempre se tendrá que pagar la deuda técnica que se ha generado.
2.3.6.1. Retrospectiva de deuda técnica
Se sabe que se debe evitar tanto como sea posible la deuda técnica, sin embargo la mayoría de los proyectos de software terminan, en mayor o menor medida, incurriendo en ella. Por esto, es importante saber cuánto se debe y la cantidad de interés que se está pagando. Para relevar esta situación se puede utilizar la retrospectiva de deuda técnica.
Al tener un conocimiento previo del proyecto, se realiza una tormenta de ideas de todas las deudas del proyecto en que se piensa que han incurrido los programadores y diseñadores que trabajaron desde el inicio del proyecto. Luego se categorizan cada una de estas deudas según esfuerzo e impacto. Vale aclarar que se trabaja sobre la deuda técnica conocida, es decir, cuanto mayor conocimiento del sistema tengan las personas involucradas en la priorización, mejor será el resultado de la retrospectiva.
En detalle, se prioriza según dos ejes o conceptos: primero el esfuerzo, que es el trabajo relativo que el equipo tendría que invertir para pagar esta deuda; y segundo el impacto, que es el efecto directo en la productividad que esta deuda provoca o la importancia que tiene para el proyecto, resolver el problema.
Se debe pagar la deuda principal para refactorizar nuestro código lo antes posible de no ser así, siempre se estará pagando sólo los intereses. El no pagar la deuda técnica repercute en la reducción de la productividad a medida que se agregan nuevos requerimientos al sistema. Se puede llegar al punto de que no se puede realizar una nueva funcionalidad requerida, ya que, el sistema no está preparado para ser modificado fácilmente. La pregunta que se hace en este momento es: ¿Qué parte de la deuda técnica se debe pagar primero?
Los cuadrantes restantes requieren un mayor análisis, el cuadrante superior derecho (Alto esfuerzo / Alto impacto) corresponde con la deuda que es compleja de refactorizar pero por la cual se está pagando un alto interés. Es decir, en la mayoría de los casos son cambios que involucran una modificación en la arquitectura del sistema, y por lo tanto afectan varios componentes del sistema. Llegado el momento esta deuda debe ser pagada sin importar el esfuerzo que se requiere. En contraposición, el cuadrante inferior izquierdo (Bajo esfuerzo/ Bajo impacto), es la parte de la deuda por la que se paga poco interés ya que no causa demasiado problema en el diseño y más allá de que el esfuerzo por solucionarlo es relativamente poco, sólo se realizará en caso de que se tenga tiempo disponible.
De acuerdo al escenario especificado en la tabla 2.2 se genera el cuadrante de retrospectiva de la figura 2.5. A juicio de los programadores del grupo de desarrollo, se le asignó un lugar en el diagrama según la valoración del grado de impacto en el sistema y el esfuerzo requerido para su implementación.
Figura 2.5. Ubicacion del escenario de ejemplo en cuadrante de deuda técnica.
2.4. Métricas y code smells
megabytes. Estos números no son más que los valores de algunos parámetros básicos. Para los sistemas orientados a objetos las más comunes son las líneas de código, el número de clases, el número de paquetes o subsistemas, el número de operaciones (métodos), etc.
Pero estas métricas por sí solas de forma aislada no permiten hacer una caracterización acertada del sistema [Lanza2007]. Es importante entender y tener en claro cómo es la correlación entre las distintas métricas y desde qué punto de referencia se realiza una comparación para realizar un análisis cualitativo.
Una aplicación, una clase, un método y cualquier otro artefacto en un sistema de software deben aplicarse de una manera armoniosa, por ejemplo, un clase tiene que implementar un número apropiado de métodos con un apropiado tamaño, complejidad y funcionalidad. Esta armonía global está compuesta por tres armonías distintas que afectan a todos los artefactos de un programa:
• Armonía de Identidad - "¿Cómo me defino?" Cada entidad en un sistema de software debe justificar su existencia: Poner en práctica un concepto específico y ¿Cómo lo hace? ¿Está haciendo demasiadas cosas o nada en absoluto?
• Armonía de Colaboración - "¿Cómo me relaciono con los demás?" Cada entidad colabora con otros para cumplir con sus tareas. ¿Que hace por su propia cuenta, o utiliza otras entidades. ¿Cómo las utiliza? ¿Utiliza demasiadas?
• Armonía de Clasificación - "¿Cómo me defino con respeto a mis antepasados y descendientes? ". Esta armonía combina elementos de armonía de identidad y colaboración en el contexto de la herencia. Por ejemplo, una subclase usa todos los servicios heredados, o se ignoran algunos de ellos?
Las métricas miden elementos estructurales y, como tales, pueden revelar síntomas ocultos. Pero siempre habrá una brecha entre los síntomas y la evaluación profunda que un experto en diseño orientado a objetos puede hacer usando estos síntomas. Por lo tanto es importante tener en cuenta a las métricas como una herramienta y como con cualquier herramienta conocer sus ventajas y desventajas.
Fowler remarca que “no hay un conjunto de métricas que sea rival para la intuición humana” [Fowler1999]. Pero hay que tener en cuenta una desventaja: “la intuición humana no escala con las dimensiones de los sistemas de la actualidad” [Lanza2007]. Sin embargo, tanto Fowler como Lanza, coinciden en la utilización de code smells como indicadores de malas prácticas de programación y en la identificación de oportunidades de refactoring.
2.4.1. Code Smells
desarrollo o aumentar el riesgo de errores o fallos en el futuro. Son el resultado de malas prácticas en la programación: generación de código duplicado, métodos muy largos, métodos que necesitan demasiados parametros, clases muy grandes, etc.
En base a los aspectos de armonía identificados anteriormente, Lanza y Marinescu clasifican los tipos de code smells en tres categorías de desarmonías: de colaboración, de identidad y de clasificación. En la figura 2.6 se muestra la relación entre los code smells y sus categorías y a continuación se describe cada uno.
Figura. 2.6. Clasificación y correlación entre code smells [Lanza2007].
2.4.1.1. Desarmonías de identidad
Son los defectos de diseño que afectan a entidades individuales como clases y métodos. La particularidad de estas anomalías es que su efecto negativo en la calidad de los elementos de diseño se puede notar al considerar estos elementos de diseño en forma aislada.
God Class
utilizando los datos de otras clases. Esto tiene un impacto negativo en la reutilización y la incomprensibilidad de esa parte del sistema.
La God Class es potencialmente dañina para el diseño de un sistema, ya que es una agregación de diferentes abstracciones y mal uso de otras clases (Clases portadoras de datos solamente) para llevar a cabo su funcionalidad. Esta situación va en contra de los principios básicos del diseño orientado a objetos que es que una clase debe tener una sola responsabilidad. En este punto es importante mencionar que una God Class es un verdadero problema si obstaculiza la evolución del sistema. Pueden existir clases que tienen las características estructurales de una God Class pero que residen en un área estable del sistema y por lo tanto no plantean un problema.
Feature Envy
Los objetos son un mecanismo para mantener juntos los datos y las operaciones que procesan esos datos. La anomalía de diseño Feature Envy se refiere a los métodos que parecen más interesados en los datos de otras clases que en los de su propia clase. Estos métodos acceden directamente o a través de métodos de acceso a una gran cantidad de datos de otras clases. Esto podría ser una señal de que el método está fuera de lugar y que debe ser trasladado a otra clase. Los datos y las operaciones que modifican o utilizan estos datos deben estar lo más cerca posible. Esta proximidad entre operaciones y datos puede ayudar a minimizar el efecto dominó, es decir, un cambio en un método provoca cambios en otros métodos y así sucesivamente.
Brain Method
A menudo, un método que empieza como un método "normal" pero luego se le añade más y más funcionalidad hasta que se sale de control, llegando a ser difícil de mantener o entender.
Los Brain Methods tienden a centralizar la funcionalidad de una clase, de la misma manera en
que una God Class centraliza la funcionalidad de todo un subsistema, o incluso a veces un sistema entero.
En el caso de los Brain Method el problema se refiere a procedimientos demasiado largos, que son difíciles de entender y depurar, y prácticamente imposible de reutilizar. Un método bien escrito debe tener una complejidad adecuada en concordancia con el propósito del método.
Brain Class
Esta anomalía de diseño se da por clases complejas que tienden a acumular una cantidad excesiva de inteligencia por lo general afectada por varios Brain Methods. Este smell puede resultar parecido a God Class ya que los dos tienen la tendencia de centralizar la inteligencia del sistema. Ambos smells hacen referencia a clases complejas pero los problemas son distintos.
Data Class
Son clases contenedoras de datos sin funcionalidad compleja, aunque otras clases dependen desmedidamente de ellas. La falta de métodos funcionalmente relevantes puede indicar que los datos y el comportamiento asociado no se mantienen en un solo lugar; esto es una señal de un diseño no orientado a objetos. Las Data Classes son la manifestación de un mal encapsulamiento de los datos, y de una baja proximidad entre los datos y la funcionalidad.
Un ejemplo de desarmonía de identidad se puede ver ampliando el ejemplo de la figura 2.1. En el sistema original, se detecta un Brain Method en el método buildMailGraph dada la gran cantidad de tareas que resuelve este método. Al implementar la refactorización del correspondiente escenario, el smell no desaparece, sino que se mueve al método build de la clase GraphBuildingStrategyMail. De todas formas, esa refactorización permitió visualizar el smell para aplicar una nueva refactorización sobre esa clase. Para eliminar esta desarmonía, se realiza una extracción de métodos que permite modularizar mejor las responsabilidades de la clase. Del método build se extraen los métodos createVertex y createEdge como se puede ver en la figura 2.7b. Se trata de una refactorización simple que elimina el smell y hace el software más comprensible y modificable.
Figura 2.7. Ejemplo de refactorización de anomalía de identidad.
2.4.1.2. Desarmonías de Colaboración
Son defectos de diseño que afectan al mismo tiempo a varias entidades por la forma en que colaboran entre ellas para llevar adelante una funcionalidad [Lanza2007]. Se entiende la colaboración de una entidad (clase) como las dependencias entrantes y salientes con otras entidades. La regla de la armonía de colaboración establece que: “Las colaboraciones deben ser sólo en términos de invocaciones de métodos y tener una extensión, intensidad y dispersión limitada”. Este tipo de síntomas guarda relación directa con el grado de acoplamiento entre varias entidades.
Intensive Coupling
Dispersive Coupling
El acoplamiento disperso se da cuando una operación está excesivamente ligada a muchas operaciones en el sistema y, adicionalmente, estos métodos proveedores están dispersos en muchas clases. Se da una colaboración poco intensa con muchas clases poco relacionadas entre sí en el sistema.
Este tipo de acoplamiento en las operaciones conduce a efectos dominó no deseados, dado que un cambio en un método dispersamente acoplado potencialmente conlleva a realizar cambios en todos los acoplamientos y por lo tanto en todas las clases de las que depende.
Shotgun Surgery
Esta disarmonía de colaboración tiene que ver con las dependencias “entrantes” de un método, es decir, cuando un método es invocado por muchos otros que a su vez están dispersos en el sistema. El principal problema que evidencia este smell es que si un cambio ocurre en el método, es probable que haya que modificar o al menos revisar todos los métodos que lo invocan desde distintos contextos del sistema. Este síntoma guarda tanta relación con la intensidad de la colaboración entre entidades como con su dispersión.
2.4.1.3. Desarmonías de Clasificación
No es suficiente para una clase estar en armonía consigo misma; también tiene que estar en armonía con su su antecesor y sus clases descendientes. La causa principal de la falta de armonía de clasificación es la idea errónea de que la herencia es principalmente un vehículo para la reutilización de código en lugar de un medio para asegurar que los objetos más específicos pueden sustituir a los más generales.
Refused Parent Bequest
Este smell se presenta cuando a la hora de ampliar o añadir una subclase no se estudia y determina qué código se puede reutilizar, lo que hay que sumar y finalmente lo que podría ser empujado a las superclases para aumentar su generalidad.
2.5. Testing
A la hora de refactorizar es una precondición esencial tener casos de test sólidos. Más allá que se tenga una herramienta de automatización de refactorings, igual se necesitan tests [Fowler1999]. Escribir buenos casos de test agiliza la programación en general se esté o no haciendo refactoring.
Si analizamos en qué gastan su tiempo los programadores, veremos que escribir código es una pequeña fracción. Un tiempo se pasa descifrando qué es los que se tiene que hacer, otro tiempo diseñando la solución, pero más tiempo se pasa depurando. Arreglar un bug es una tarea sencilla, encontrar el bug es la tarea difícil. Todos los programadores han pasado largas horas para encontrar un error alguna vez. Y existe la posibilidad de, al corregir el error, generar otro que se descubrirá más tarde y se tenga que pasar de vuelta por el mismo problema.
totalmente automáticos, es decir, no debe ver la salida de cada uno de los test y verificar que esta salida se encuentre en entre los resultados esperados. Los test deben verificar su resultado e indicar si funciona bien o hay alguna falla en la clase. Se les deben pasar los parámetros de entrada y chequear su salida para que arrojen los datos esperados.
Ejecutar los test cada vez que se compila el proyecto reflejará una mejora de la productividad, ya que, en caso de que falle un test ya se sabe que el error está en el trabajo que se ha hecho desde la última vez que se corrieron los tests. Debido a que el código está fresco en la mente del programador y es una pequeña porción, el error es más fácil de encontrar. Errores que podían tardar una hora o más para encontrarlos ahora toman un par de minutos a lo sumo. Teniendo en cuenta la frecuencia en que se ejecutan, un conjunto de test es un potente detector de errores que minimiza el tiempo que se tarda en encontrar un error.
Uno de los momentos más útiles para escribir casos de test es antes de empezar la programación. Cuando se tenga que añadir una característica al sistema se puede comenzar escribiendo el test. Al escribir antes el test se está pensando en qué resultado se espera independientemente de cómo se implementa la nueva función. Escribir el test se concentra en la interfaz en lugar de la aplicación. También significa que el programador tiene un punto claro en el que termina de codificar: cuando el test funciona [Beck2000].
En el ejemplo de refactoring de la figura 2.1, un posible caso de test podría ser que dada una misma fuente de información de correos electrónicos, ejecutar el método buildMailGraph de la clase GraphBuilder y medir que la cantidad de arcos, vértices y las propiedades asociadas a los mismos sean los esperados. Luego de realizado el refactoring, se vuelve a ejecutar el test para comprobar que los resultados son los esperados. Este test validará que el cambio se ha realizado exitosamente sin afectar el funcionamiento observable del sistema.
En síntesis, refactorizar una aplicación requiere testeo, si desea refactorizar se debe escribir los tests. Aunque no se pueda hacer una cobertura del 100% del sistema, una pequeña cantidad puede tener grandes beneficios. Para hacer más fácil este trabajo se pueden utilizar frameworks especializados en automatización de testing, según la tecnología que se esté utilizando para el sistema.
2.6. Resumen
En este capítulo se han introducido los conceptos que dan sustento a este trabajo. Teniendo en cuenta cómo es la evolución de los sistema de software y los problemas que la adición de nuevas funcionalidades pueden causar en la calidad con el paso del tiempo. Se encuentra en el refactoring una herramienta útil para mantener y mejorar la modificabilidad del sistema, preparándolo para que la adición de nuevas funcionalidades se haga de forma más rápida y sencilla.
3. Trabajos Relacionados
En esta sección se describe el estado del arte relacionado a la refactorización de sistemas en la actualidad. Con el objetivo de presentar los distintos enfoques analizados, los mismos se explicarán en función de los aportes que realizan en las temáticas que más interesan al desarrollo de este trabajo final.
Los trabajos presentados pretenden mejorar la calidad de los sistemas principalmente mediante la aplicación de soluciones para favorecer la modificabilidad de los artefactos, intentando aumentar en consecuencia la legibilidad y facilidad de incorporar nuevas funcionalidades en los mismos.
Algunos de estos trabajos brindan indicios de problemas que resultan útiles para guiar y enfocar el proceso de refactoring sobre los sistemas. Algunos se basan exclusivamente en la identificación de oportunidades de refactorización y otros evalúan las mejoras de calidad obtenidas a partir de la implementación de distintos tipos de refactoring.
Este capítulo se organiza en cuatro secciones que permiten revisar distintas experiencias en materia de refactorización de sistemas de software. Las mismas son: aspectos generales del refactoring de software, enfoques de refactoring, impacto de los refactorings en la calidad del software y refactoring guiados por code smells.
3.1. Aspectos generales del refactoring de software
El campo de la refactorización de sistemas ha evolucionado mucho en los últimos años. En esta sección se tendrá una visión general de la investigación existente en materia de refactorización de software en la actualidad.
3.1.1. Formalización de los procesos de refactoring
Existen relevamientos de refactoring basados en diferentes criterios como las actividades de refactorización soportadas, técnicas específicas usadas para soportar esas actividades, tipos de artefactos que se pueden refactorizar, asuntos importantes a tener en cuenta al desarrollar herramientas de soporte a la refactorización y el efecto del refactoring en el proceso del software [Mens2004]. En general, se ha identificado una necesidad de formalismos, procesos, métodos y herramientas que se ocupan de la refactorización de una manera más consistente, genérica, escalable y flexible. Por eso, el objetivo de este trabajo es definir un proceso de refactorización.
3.1.2. Desafíos y beneficios de refactorizar
del historial de versiones, revelando que la definición de refactorización en la práctica no se limita a una definición rigurosa de las transformaciones de código y que los desarrolladores perciben que la refactorización implica costos y riesgos considerables [Kim2012]. Las figuras 3.1 y 3.2 muestran los resultados de las encuestas a los desarrolladores respecto a los riesgos y los beneficios de refactorizar respectivamente.
Figura 3.1. Factores de riesgo asociados a refactorizar [Kim2012].
Figura 3.2. Varios tipos de beneficios de refactorizar experimentados por los desarrolladores [Kim2012].
resultados positivos, aunque para esto no se utilizaron herramientas de análisis automáticas ni de apoyo para refactorizaciones semi-automáticas.
3.1.3. Dos tácticas de refactoring
Para hacer el refactoring de software más eficiente, es bueno saber cómo son realizados los refactorings en el mundo real. En general, existen dos grandes tácticas para realizar refactorings. La primera es la utilizada en la metodología XP, realizando pequeños pasos, llamada floss refactoring. Esta táctica es bien conocida, sobretodo entre los defensores de XP. La otra táctica, llamada root canal refactoring, se trata de dedicar un periodo de tiempo de desarrollo exclusivamente a la refactorización del sistema. A partir del análisis estadístico de una gran cantidad de proyectos de software, se pudo determinar que aproximadamente un 11,5% de los refactorings son de tipo root canal, mientras que el restante 88.5% es de tipo floss refactoring [Liu2012]. Este estudio, muestra las distintas tácticas, sus características y las preferencias de los desarrolladores. Muestra también la falta de conocimiento sobre las tácticas de tipo “root canal” que, en general, implican un análisis más profundo de las debilidades del software para aplicar mejoras significativas de calidad.
Las herramientas de refactorización, si se utilizan, pueden mejorar la velocidad y la precisión con la que creamos y mantenemos software. En la práctica, las herramientas no se utilizan tanto como podría ser ya que a veces no se alinean con el floss refactoring, la táctica de refactorización preferida por la mayoría de los programadores. Algunas herramientas se han diseñado de una manera que las hace más apropiadas para la refactorización “root canal” que para refactorización “floss”, es por esta razón que las herramientas de refactorización no se utilizan tanto como se podría esperar.
3.1.4. Herramientas para cada táctica de refactoring
Con el fin de aprovechar mejor las herramientas de soporte, se han propuesto una serie de principios que caracterizan a exitosas herramientas de floss refactoring, que pueden ayudar a los programadores a elegir herramientas de refactorización más adecuadas y también ayudan a diseñar herramientas que se ajusten mejor al propósito de los programadores [MurphyHill2008]. Si bien se aprecia una interesante perspectiva de la actualidad de la refactorización, sólo se focaliza en el tipo de herramientas a utilizar dependiendo de qué táctica se aplique. Sería bueno también evaluar las ventajas y desventajas de cada táctica y promover las mejores prácticas de desarrollo.
3.1.5. Refactoring guiado por métricas
Ciertas acciones de refactorización como Extract Class tienden a crear más clases, lo que aumenta la profundidad de la herencia del árbol, lo que aumenta la complejidad general. Otras acciones de refactorización, tales como Extract Method, pueden ser utilizados para reducir líneas totales de código y reducir la duplicación en el código, lo que disminuye el tamaño de la aplicación y aumenta la mantenibilidad, con lo que aparentemente aumentar la calidad global de software para la aplicación. Sabiendo esto, faltaría aún investigar en profundidad la relación entre ambos tipos de métricas, estudiar qué técnicas, enfoques y herramientas de refactorización se aplican mejor para optimizar un tipo u otro de métricas.
3.2. Enfoques de refactoring
En el presente trabajo final, se define un proceso de refactorización de sistemas de software guiado por code smells y escenarios de calidad. A continuación se presentan y evalúan enfoques de refactoring guiados por distintas características.
3.2.1. Identificación de oportunidades de refactoring
Algunos expertos, afirman que se pueden identificar tres pasos para el proceso de refactorización [Tourwé2003].
● Detectar cuándo una aplicación debería ser refactorizada.
● Identificar qué refactoring o refactorings deberían ser aplicados y dónde.
● Implementar los refactorings. Etapa que en general se divide en 2 fases: verificar las pre condiciones apropiadas para que el refactoring no afecte la funcionalidad del sistema y efectivamente aplicar las refactorizaciones.
En general, los entornos de desarrollo modernos proveen soporte para la segunda fase del paso 3, pero no para el resto. El soporte automatizado puede facilitar la identificación de oportunidades de refactorización y más específicamente sugerir qué tipo de refactoring se debe implementar. Si bien existen herramientas para la detección de malos síntomas en un sistema, aún es necesario el análisis del desarrollador para discriminar y ponderar los problemas detectados, que pueden ser una gran cantidad, distribuidos en todo el sistema.
En este trabajo final se tomó en cuenta esta serie de pasos para definir un proceso de refactorización basado en el análisis objetivo de las métricas y en el análisis subjetivo realizado por los desarrolladores.