Tal vez esté escribiendo sobre algo obvio para muchos, pero quizá no para todos. La refactorización es, en mi opinión, un tema complicado porque implica cambiar el código sin afectar a su funcionamiento.
Por lo tanto, para algunos es incomprensible que refactorización es en realidad un área de la programación, y también una parte muy importante del trabajo del programador. El código está en constante evolución, se modificará siempre que exista la posibilidad de añadir nuevas funcionalidades. Sin embargo, puede adoptar una forma que ya no permita añadir eficazmente nuevas funcionalidades y sería más fácil reescribir todo el programa.
¿Qué es la refactorización?
Normalmente, la respuesta que se oye es que se trata de cambiar la estructura del código aplicando una serie de transformaciones de refactorización sin afectar al comportamiento observable del código. Esto es cierto. Recientemente, también me encontré con una definición de Martin Fowler en su libro "Mejorar el diseño del código existente" donde describe refactorización como "hacer grandes cambios en pequeños pasos". Describe refactorización como un cambio de código que no afecte a su funcionamiento, pero insiste en que debe hacerse en pequeños pasos.
El libro también defiende que refactorización no afecta al funcionamiento del código y señala que no tiene ningún efecto sobre la superación de las pruebas en ningún momento. Describe paso a paso cómo realizar de forma segura refactorización. Me gustó su libro porque describe trucos sencillos que se pueden utilizar en el trabajo diario.
¿Por qué es necesaria la refactorización?
La mayoría de las veces, puede necesitarlo cuando quiere introducir una nueva funcionalidad y el código en su versión actual no lo permite o sería más difícil sin cambios en el código. También es útil en casos en los que añadir más funcionalidades no es rentable en términos de tiempo, es decir, sería más rápido reescribir el código desde cero. Creo que a veces se olvida que refactorización puede hacer que el código sea más limpio y legible. Martin escribe en su libro cómo realiza la refactorización cuando siente olores desagradables en el código y, cómo lo dice, "siempre deja espacio para lo mejor". Y aquí me sorprendió al ver la refactorización como un elemento del trabajo cotidiano con el código. Para mí, los códigos son a menudo incomprensible, la lectura es un poco de experiencia como el código es a menudo poco intuitivo.
El rasgo distintivo de un programa bien diseñado es su modularidadgracias a la cual basta con conocer sólo una pequeña parte del código para introducir la mayoría de las modificaciones. La modularidad también facilita la incorporación de nuevas personas y permite empezar a trabajar de forma más eficiente. Para lograr esta modularidad, los elementos relacionados del programa deben agruparse, y las conexiones deben ser comprensibles y fáciles de encontrar. No existe una única regla general sobre cómo hacerlo. A medida que se conoce y comprende mejor cómo debe funcionar el código, se pueden agrupar los elementos, pero a veces también hay que probar y comprobar.
Una de las reglas de refactorización en YAGNIes un acrónimo de "You Aren't Gonna Need It" (No lo vas a necesitar) y procede de Programación eXtrema (XP) utilizado principalmente en Ágildesarrollo de software equipos. Resumiendo, YAGNI dice que sólo se deben hacer cosas actualizadas. Esto significa básicamente que aunque algo pueda ser necesario en el futuro, no debe hacerse ahora. Pero tampoco podemos aplastar las ampliaciones posteriores y aquí es donde la modularidad cobra importancia.
Al hablar de refactorizaciónEn este sentido, hay que mencionar uno de los elementos más esenciales: las pruebas. En refactorizaciónnecesitamos saber que el código sigue funcionando, porque refactorización no cambia su funcionamiento, sino su estructura, por lo que todas las pruebas deben ser superadas. Es mejor ejecutar pruebas para la parte del código en la que estamos trabajando después de cada pequeña transformación. Nos da una confirmación de que todo funciona como debe y acorta el tiempo de toda la operación. Esto es lo que Martin habla en su libro - ejecutar pruebas tan a menudo como sea posible para no dar un paso atrás y perder el tiempo buscando una transformación que rompió algo.
Refactorización del código sin pruebas es un coñazo y hay muchas posibilidades de que algo salga mal. Si es posible, lo mejor sería añadir al menos algunas pruebas básicas que nos den un poco de seguridad de que el código funciona.
Las transformaciones enumeradas a continuación son sólo ejemplos, pero resultan realmente útiles en la programación diaria:
Extracción de funciones y extracción de variables: si la función es demasiado larga, compruebe si hay alguna función menor que pueda extraerse. Lo mismo ocurre con las líneas largas. Estas transformaciones pueden ayudar a encontrar duplicaciones en el código. Gracias a las funciones menores, el código se vuelve más claro y comprensible,
Cambio de nombre de funciones y variables: utilizar la convención de nomenclatura correcta es esencial para programar bien. Los nombres de las variables, cuando están bien elegidos, pueden decir mucho sobre el código,
Agrupación de las funciones en una clase - este cambio es útil cuando dos clases realizan operaciones similares, ya que puede acortar la longitud de la clase,
Anulación de la sentencia anidada: si la condición se verifica para un caso especial, emita una sentencia return cuando se produzca la condición. Este tipo de pruebas a menudo se denominan cláusula de guarda. Sustituir una sentencia condicional anidada por una sentencia de salida cambia el énfasis en el código. La construcción if-else asigna el mismo peso a ambas variantes. Para la persona que lee el código, es un mensaje de que cada una de ellas es igualmente probable e importante,
Introducir un caso especial: si utiliza algunas condiciones en su código muchas veces, puede merecer la pena crear una estructura independiente para ellas. La mayoría de las comprobaciones de casos especiales pueden sustituirse por simples llamadas a funciones. A menudo, el valor común que requiere un tratamiento especial es null. Por ello, este patrón se denomina con frecuencia objeto nulo. Sin embargo, este enfoque puede utilizarse en cualquier caso especial,
Sustitución del polimorfismo de instrucción condicional.
Ejemplo
Este es un artículo sobre refactorización y se necesita un ejemplo. A continuación quiero mostrar un ejemplo sencillo de refactorización con el uso de Anulación de la declaración anidada y Sustitución del polimorfismo de instrucción condicional. Supongamos que tenemos una función de programa que devuelve un hash con información sobre cómo regar las plantas en la vida real. Dicha información probablemente estaría en el modelo, pero para este ejemplo, la tenemos en la función.
def riego_info(planta)
resultado = {}
if planta.¿es? Suculenta || plant.is_a? Cactus
result = { cantidad_de_agua: "Un poco" , how_to: "Desde abajo", watering_duration: "2 semanas" }
elsif plant.is_a? Alocasia | plant.is_a? Maranta
result = { cantidad_de_agua: "Gran cantidad", how_to: "Como prefieras", watering_duration: "5 días" }
elsif planta.is_a? Peperomia
result = { cantidad_de_agua: "Cantidad dicente",
how_to: "¡Desde abajo! no les gusta el agua en las hojas",
watering_duration: "1 semana" }
si no
resultado = { cantidad_de_agua "Cantidad dicente",
how_to: "Como prefieras",
duración_del_riego: "1 semana"
}
end
devolver resultado
fin
La idea es cambiar si volver:
si planta.isa? Suculenta || ¿planta.isa? Cactus
resultado = { wateramount: "Un poco" , howto: "Desde el fondo",
A
return { cantidad_de_agua: "Un poco " , como_hacer: "Desde abajo",duración_riego: "2 semanas" } if plant.is_a? Suculenta || plant.is_a? Cactus
devolver { aguacantidad: "Un poco " , cómoa: "Desde abajo", riegoduración: "2 semanas" } if plant.isa? Suculenta || plant.is_a? Cactus
Y así con todo, hasta llegar a una función parecida a ésta:
def riego_info(planta)
return resultado = { cantidad de agua: "Un poco " , howto: "Desde abajo", wateringduration: "2 semanas" } if planta.isa? Suculenta || plant.is_a? Cactus
return resultado = { cantidad de agua: "Cantidad grande", howto: "Como prefieras", wateringduration: "5 días" } if planta.isa? Alocasia || plant.is_a? Maranta
return resultado = { cantidad_de_agua: "Cantidad dicente",
howto: "¡Desde abajo! no les gusta el agua en las hojas",
wateringduration: "1 semana" } if plant.is_a? Peperomia
return resultado = { cantidad_de_agua: "Cantidad dicente",
how_to: "Como prefieras",
duración_del_riego: "1 semana"
}
end
Al final, ya teníamos un resultado de vuelta. Y un buen hábito es hacer esto paso a paso y probar cada cambio. Podrías sustituir este bloque if por un switch case y quedaría mejor inmediatamente, y no tendrías que comprobar todos los ifs cada vez. Se vería así:
def riego_info(planta)
swich planta.clase.a_cadena
case Suculenta, Cactus
{ cantidad de agua: "Un poco" , howto: "Desde abajo", watering_duration: "2 semanas" }
caso Alocasia, Maranta
{ wateramount: "Gran cantidad", howto: "Como prefieras", duración_riego: "5 días" }
caso Peperomia
{ cantidad_agua: "Cantidad dicente",
cómo: "¡Desde abajo! No les gusta el agua en las hojas",
watering_duration: "1 semana" }
si no
{ cantidad_de_agua: "Cantidad dicente",
how_to: "Como prefieras",
duración_del_riego: "1 semana" }
end
fin
Y luego puede aplicar el Sustitución del polimorfismo de instrucción condicional. Se trata de crear una clase con una función que devuelva el valor correcto y los conmute en sus lugares adecuados.
clase Suculenta
...
def riego_info()
return { cantidad de agua: "Un poco " , howto: "Desde abajo", watering_duration: "2 semanas" }
end
end
clase Cactus
...
def riego_info()
return { cantidad de agua: "Un poco " , howto: "Desde abajo", watering_duration: "2 semanas" }
end
end
clase Alocasia
...
def información_de_riego
return { cantidad de agua: "Cantidad grande", como: "Como prefieras", watering_duration: "5 días" }
end
fin
clase Maranta
...
def riego_info
return { cantidad de agua: "Cantidad grande", como: "Como prefieras", watering_duration: "5 días" }
end
fin
clase Peperomia
...
def riego_info
return { cantidad_agua: "Cantidad dicente",
how_to: "¡Desde abajo! no les gusta el agua en las hojas",
watering_duration: "1 semana" }
end
fin
clase Planta
...
def información_de_riego
return { cantidad_agua: "Cantidad dicente",
how_to: "Como prefieras",
duración_del_riego: "1 semana" }
end
fin
Y en la watering_infofunction principal, el código tendrá este aspecto:
def riego_info(planta)
plant.map(&:watering_info)
fin
Por supuesto, esta función puede eliminarse y sustituirse por su contenido. Con este ejemplo, quería presentar el general patrón de refactorización.
Resumen
Refactorización es un gran tema. Espero que este artículo haya sido un incentivo para leer más. Estos habilidades de refactorización te ayudarán a detectar tus errores y a mejorar tu taller de código limpio. Recomiendo leer el libro de Martin (Improving the Design of Existing Code), que es un conjunto bastante básico y útil de reglas de refactorización. El autor muestra varias transformaciones paso a paso con una explicación completa y motivación y consejos sobre cómo evitar errores en refactorización. Debido a su versatilidad, es un libro delicioso para frontend y desarrolladores backend.