Durante el proceso de de­sa­rro­llo de una apli­ca­ción, en el código fuente se van acu­mu­la­n­do elementos con una es­tru­c­tu­ra de­fi­cie­n­te que ponen en riesgo la apli­ca­bi­li­dad y la co­m­pa­ti­bi­li­dad del programa. Para so­lu­cio­nar este problema, hay dos opciones: o bien volver a escribir el código desde cero o realizar una re­es­tru­c­tu­ra­ción en pequeños pasos. Cada vez más pro­gra­ma­do­res y empresas optan por la segunda opción, es decir, la re­fa­c­to­ri­za­ción del código, para optimizar un software activo a largo plazo y hacerlo más legible y claro para otros pro­gra­ma­do­res.

La re­fa­c­to­ri­za­ción (o re­fa­c­to­ri­ng, en inglés) plantea una pregunta: ¿qué problema se quiere so­lu­cio­nar usando qué método? Hoy en día, la re­fa­c­to­ri­za­ción ya forma parte de las lecciones básicas del apre­n­di­za­je de la pro­gra­ma­ción y es cada vez más relevante, pero ¿qué métodos utiliza y qué ventajas e in­co­n­ve­nie­n­tes trae consigo?

¿Qué es la re­fa­c­to­ri­za­ción?

Programar software conlleva un proceso largo que suele in­vo­lu­crar, al menos pa­r­cia­l­me­n­te, a varios de­sa­rro­lla­do­res, por lo que durante el de­sa­rro­llo el código fuente a menudo se amplía y modifica. Si además de estos cambios, tenemos en cuenta que se trabaja a co­n­tra­rre­loj y utilizan ciertas prácticas an­ti­cua­das, ob­se­r­va­mos como resultado una acu­mu­la­ción de elementos de­fe­c­tuo­sos en el código fuente, los llamados code smells. Estos puntos débiles, que van au­me­n­ta­n­do a medida que avanza el proceso, ponen en peligro la fu­n­cio­na­li­dad y la co­m­pa­ti­bi­li­dad del software en cuestión. Para evitar esta erosión continua del programa, se utiliza la re­fa­c­to­ri­za­ción o re­fa­c­to­ri­ng.

La re­fa­c­to­ri­za­ción podría co­m­pa­rar­se con la co­rre­c­ción de un libro: el producto final de la co­rre­c­ción no es un nuevo libro, sino el mismo texto, pero más co­m­pre­n­si­ble. Así, al igual que en la co­rre­c­ción de un libro se usan pro­ce­di­mie­n­tos como la re­fo­r­mu­la­ción y la re­es­tru­c­tu­ra­ción o eli­mi­na­ción de frases, en la re­fa­c­to­ri­za­ción de código se aplican métodos como la en­ca­p­su­la­ción, el re­fo­r­ma­teo o la ex­tra­c­ción para optimizar el código sin cambiar su contenido.

Este proceso resulta mucho más económico que re­es­cri­bir la es­tru­c­tu­ra del código desde cero. La re­fa­c­to­ri­za­ción desempeña un papel es­pe­cia­l­me­n­te im­po­r­ta­n­te en el de­sa­rro­llo iterativo e in­cre­me­n­tal, así como en el de­sa­rro­llo ágil de software, ya que este modelo cíclico lleva a los pro­gra­ma­do­res a modificar el software una y otra vez. En este proceso, la re­fa­c­to­ri­za­ción es un paso clave.

Cuando el código fuente decae: el código espagueti

Antes que nada, es im­po­r­ta­n­te entender que un código puede degenerar a lo largo del tiempo y co­n­ve­r­ti­r­se en un infame código espagueti. Ya sea por falta de tiempo, por falta de ex­pe­rie­n­cia o por di­re­c­tri­ces poco claras, las órdenes in­ne­ce­sa­ria­me­n­te complejas en la pro­gra­ma­ción del código acaban ob­s­ta­cu­li­za­n­do su fu­n­cio­na­li­dad. Cuanto más rápido y complejo sea el ámbito de apli­ca­ción de un código, más se ero­sio­na­rá.

El término código espagueti hace re­fe­re­n­cia a códigos fuente confusos y de difícil lectura, cuya es­tru­c­tu­ra es difícil de co­m­pre­n­der para los pro­gra­ma­do­res. Algunos ejemplos típicos de elementos que complican el código son las órdenes de salto (GOTO) re­du­n­da­n­tes, que indican al programa que vaya saltando de un sitio a otro en el código; los bucles for/while y los comandos if.

Co­n­cre­ta­me­n­te, los proyectos en los que trabajan muchos de­sa­rro­lla­do­res suelen generar un código poco legible. Si un código que ya de por sí pre­se­n­ta­ba ciertas im­pe­r­fe­c­cio­nes pasa por muchas manos, es difícil evitar que se encadenen mo­di­fi­ca­cio­nes a modo de parche y que, fi­na­l­me­n­te, se requiera una cara revisión o review del código para co­rre­gi­r­lo. En el peor de los casos, el código espagueti puede poner en peligro todo el proceso de de­sa­rro­llo del software, llegando a un punto en el que ni siquiera la re­fa­c­to­ri­za­ción puede resolver el problema.

Los llamados code smells y el code rot (es decir, los defectos y la erosión del software) no tienen por qué ser tan preo­cu­pa­n­tes. Con el paso del tiempo, si un código contiene muchos elementos in­ne­ce­sa­rios, puede empezar a apestar, por así decirlo. Las secciones poco claras de la es­tru­c­tu­ra empeoran cada vez que un pro­gra­ma­dor nuevo se pone a tra­ba­jar­las o amplía el código. Es por tanto necesario realizar un re­fa­c­to­ri­ng en cuanto aparezcan los primeros code smells, ya que si no el código seguirá ero­sio­ná­n­do­se y perderá su fu­n­cio­na­li­dad a causa del code rot (el proceso de pu­tre­fa­c­ción, traducido li­te­ra­l­me­n­te).

¿Cuál es el objetivo de la re­fa­c­to­ri­za­ción?

La re­fa­c­to­ri­za­ción siempre tiene el sencillo y claro propósito de mejorar el código. Con un código más efectivo, puede fa­ci­li­tar­se la in­te­gra­ción de nuevos elementos sin incurrir en errores nuevos. Además, cuanto más fácil les resulte a los pro­gra­ma­do­res leer el código, más rápido se fa­mi­lia­ri­za­rán con él y podrán ide­n­ti­fi­car y evitar los bugs de forma más eficiente. Otro objetivo de la re­fa­c­to­ri­za­ción es mejorar el análisis de errores y la necesidad de ma­n­te­ni­mie­n­to del software. Poner a prueba el código ahorra esfuerzo a los pro­gra­ma­do­res.

¿Qué fuentes de error corrige la re­fa­c­to­ri­za­ción?

Los métodos aplicados en la re­fa­c­to­ri­za­ción son tan variados como los errores que tratan de corregir. De manera general, la re­fa­c­to­ri­za­ción del código se guía por sus errores y va mostrando los pasos ne­ce­sa­rios para acortar o eliminar procesos de co­rre­c­ción. Algunas de las fuentes de error que pueden co­rre­gi­r­se mediante re­fa­c­to­ri­ng son las si­guie­n­tes:

  • Es­tru­c­tu­ras co­m­pli­ca­das o demasiado largas: cadenas y bloques de comandos tan largos que la lógica interna del software se vuelve in­co­m­pre­n­si­ble para lectores externos.
  • Re­du­n­da­n­cias en el código: los códigos poco claros suelen contener re­pe­ti­cio­nes que han de co­rre­gi­r­se una a una durante el ma­n­te­ni­mie­n­to, por lo que consumen mucho tiempo y recursos.
  • Listas de pa­rá­me­tros demasiado largas: los objetos no se asignan di­re­c­ta­me­n­te a un método, sino que se indican sus atributos en una lista de pa­rá­me­tros.
  • Clases con de­ma­sia­das funciones: clases con de­ma­sia­das funciones definidas como método, también llamadas god objects, que hacen que adaptar el software se vuelva casi imposible.
  • Clases con funciones in­su­fi­cie­n­tes: clases con tan pocas funciones definidas como método que se vuelven in­ne­ce­sa­rias.
  • Código demasiado general con casos es­pe­cia­les: funciones con casos es­pe­cia­les demasiado es­pe­cí­fi­cos que apenas se usan y que, por lo tanto, di­fi­cu­l­tan la in­co­r­po­ra­ción de am­plia­cio­nes ne­ce­sa­rias.
  • Middle man: una clase separada actúa como in­te­r­me­dia­ria entre los métodos y las distintas clases, en lugar de di­re­c­cio­nar las so­li­ci­tu­des de los métodos di­re­c­ta­me­n­te a una clase.

¿Cómo se aplica la re­fa­c­to­ri­za­ción?

La re­fa­c­to­ri­za­ción debe llevarse a cabo antes de modificar una función del programa. En el mejor de los casos, debe rea­li­zar­se en muy pocos pasos y co­m­pro­ba­n­do cada mo­di­fi­ca­ción del código mediante procesos de de­sa­rro­llo de software como el de­sa­rro­llo guiado por pruebas (TDD, por sus siglas en inglés) y la in­te­gra­ción continua (CI). En pocas palabras, el TDD y la CI se encargan de poner a prueba los nuevos segmentos de código creados por los pro­gra­ma­do­res, que luego son in­te­gra­dos y cuya fu­n­cio­na­li­dad es evaluada mediante procesos de prueba, a menudo au­to­ma­ti­za­dos.

Por regla general, un programa ha de ser mo­di­fi­ca­do en pocos pasos y desde dentro, sin que su función externa se vea afectada. Tras cada cambio, debe rea­li­zar­se un test que esté au­to­ma­ti­za­do en la medida de lo posible.

¿Qué técnicas existen?

Hay mu­chí­si­mas técnicas concretas de re­fa­c­to­ri­za­ción. Para co­no­ce­r­las todas, se puede consultar la exhau­s­ti­va obra sobre este tema de Martin Fowler y Kent Beck: Re­fa­c­to­ri­ng: Improving the Design of Existing Code. A co­n­ti­nua­ción, pre­se­n­ta­mos un resumen:

De­sa­rro­llo rojo-verde:

El llamado de­sa­rro­llo rojo-verde es un método ágil de de­sa­rro­llo de software basado en test. Suele aplicarse cuando se quiere integrar una nueva función en un código existente. El rojo es el símbolo del primer test, realizado antes de la im­ple­me­n­ta­ción de la nueva función. El verde, por su parte, se refiere al segmento de código más sencillo que requiere la función para superar el test. A co­n­ti­nua­ción, se realiza una am­plia­ción con test co­n­s­ta­n­tes para descartar el código de­fe­c­tuo­so y aumentar así la fu­n­cio­na­li­dad. El de­sa­rro­llo rojo-verde es un elemento clave para la re­fa­c­to­ri­za­ción continua en el de­sa­rro­llo continuo de software.

Branching by ab­s­tra­c­tion

Este método de re­fa­c­to­ri­za­ción aplica cambios graduales a un sistema y va mo­di­fi­ca­n­do elementos an­ti­cua­dos del código y su­s­ti­tu­yé­n­do­los por segmentos nuevos. El branching by ab­s­tra­c­tion suele uti­li­zar­se cuando se realizan cambios grandes que afectan a la jerarquía de clases, a las herencias y a la ex­tra­c­ción. Al im­ple­me­n­tar una ab­s­tra­c­ción que se mantiene enlazada con una im­ple­me­n­ta­ción antigua, pueden enlazarse con la ab­s­tra­c­ción otros métodos y clases. De esta manera, es posible sustituir la fu­n­cio­na­li­dad del segmento antiguo por la ab­s­tra­c­ción.

A menudo, este proceso se lleva a cabo mediante métodos de pull-up o push-down. La función nueva y mejorada se enlaza con la ab­s­tra­c­ción y se le tra­n­s­mi­ten los enlaces. Al hacerlo, o bien se tra­n­s­fo­r­ma una subclase en una clase superior (pull-up) o se divide una clase superior en subclases (push-down).

Fi­na­l­me­n­te, pueden borrarse las funciones antiguas sin poner en peligro la fu­n­cio­na­li­dad del conjunto. Gracias a estos cambios a pequeña escala, el sistema funciona igual que antes mientras se van re­m­pla­za­n­do uno a uno los segmentos de­fe­c­tuo­sos del código por otros mejorados.

Combinar métodos

La re­fa­c­to­ri­za­ción de código tiene el objetivo de que los métodos puedan leerse de la manera más fácil posible. En el mejor de los casos, los pro­gra­ma­do­res externos que lean el código deberían poder captar la lógica interna del método. Para combinar métodos de forma eficiente, el re­fa­c­to­ri­ng cuenta con diversas técnicas. El objetivo de cada cambio es unificar métodos, eliminar re­du­n­da­n­cias y dividir métodos largos en segmentos separados que puedan ser mo­di­fi­ca­dos fá­ci­l­me­n­te en el futuro.

Algunas de estas técnicas son las si­guie­n­tes:

  • Extraer métodos
  • Convertir métodos a inline
  • Eliminar variables te­m­po­ra­les
  • Sustituir variables te­m­po­ra­les por métodos de solicitud
  • In­tro­du­cir variables de­s­cri­p­ti­vas
  • Separar variables te­m­po­ra­les
  • Eliminar re­di­re­c­cio­na­mie­n­tos a variables de parámetro
  • Sustituir métodos por un objeto método
  • Sustituir el algoritmo

Mover pro­pie­da­des entre clases

Para mejorar un código, es necesario poder mover atributos o métodos entre clases. Las si­guie­n­tes técnicas sirven para realizar estos cambios:

  • Mover el método
  • Mover el atributo
  • Extraer la clase
  • Convertir una clase a inline
  • Ocultar el delegate
  • Eliminar una clase en el centro
  • In­tro­du­cir métodos ajenos
  • In­tro­du­cir una am­plia­ción local

Or­ga­ni­za­ción de los datos

El objetivo de este método es cla­si­fi­car los datos en clases, que deben ser tan pequeñas y fáciles de co­m­pre­n­der como sea posible. Deben ser eli­mi­na­dos y divididos en clases lógicas los enlaces in­ne­ce­sa­rios entre clases que pe­r­ju­di­quen la fu­n­cio­na­li­dad del software ante pequeños cambios.

Algunos ejemplos de este tipo de técnicas son los si­guie­n­tes:

  • En­ca­p­su­lar los propios accesos a atributos
  • Sustituir un atributo propio por una re­fe­re­n­cia de objeto
  • Sustituir un valor por una re­fe­re­n­cia
  • Sustituir una re­fe­re­n­cia por un valor
  • Agrupar datos ob­se­r­va­bles
  • En­ca­p­su­lar atributos
  • Sustituir un conjunto de datos por una clase de datos

Si­m­pli­fi­ca­ción de fórmulas co­n­di­cio­na­das

Las fórmulas co­n­di­cio­na­das deberían si­m­pli­fi­car­se tanto como sea posible durante la re­fa­c­to­ri­za­ción del código. Para hacerlo, existen varias técnicas:

  • Dividir las co­n­di­cio­nes
  • Agrupar fórmulas co­n­di­cio­na­das
  • Agrupar comandos re­du­n­da­n­tes en fórmulas co­n­di­cio­na­das
  • Eliminar elementos de control
  • Sustituir co­n­di­cio­nes activas por guardias
  • Sustituir di­s­ti­n­cio­nes de caso por po­li­mo­r­fi­s­mo
  • In­tro­du­c­ción de objetos cero

Si­m­pli­fi­ca­ción de las llamadas a método

Las llamadas o so­li­ci­tu­des a método pueden eje­cu­tar­se más fácil y rá­pi­da­me­n­te con las si­guie­n­tes técnicas:

  • Cambiar el nombre de los métodos
  • Añadir pa­rá­me­tros
  • Eliminar pa­rá­me­tros
  • Sustituir pa­rá­me­tros por métodos ex­plí­ci­tos
  • Sustituir código de­fe­c­tuo­so por ex­ce­p­cio­nes

Ejemplo de re­fa­c­to­ri­za­ción: cambiar el nombre a un método

En el siguiente ejemplo, el nombre original del método no deja clara su función. El método debería revelar el código postal de la dirección de una oficina, pero esta tarea no está indicada di­re­c­ta­me­n­te en el código. Para fo­r­mu­lar­lo de manera más clara, puede re­fa­c­to­ri­zar­se el código ca­m­biá­n­do­le el nombre al método.

Antes:

String getPostalCode() {
	return (theOfficePostalCode+“/“+theOfficeNumber);
}
System.out.print(getPostalCode());

Después:

String getOfficePostalCode() {
	return (theOfficePostalCode+“/“+theOfficeNumber);
}
System.out.print(getOfficePostalCode());

Re­fa­c­to­ri­za­ción: ¿qué ventajas y de­s­ve­n­ta­jas presenta?

Ventajas De­s­ve­n­ta­jas
Una mejor co­m­pre­n­sión facilita el ma­n­te­ni­mie­n­to y la am­plia­ción del software. Una re­fa­c­to­ri­za­ción imprecisa podría generar nuevos bugs y errores en el código.
La re­es­tru­c­tu­ra­ción del código fuente puede rea­li­zar­se sin cambiar la fu­n­cio­na­li­dad. No existe una de­fi­ni­ción clara de qué es un código limpio o bien es­tru­c­tu­ra­do.
La mejora en la le­gi­bi­li­dad del código facilita que otros pro­gra­ma­do­res lo co­m­pre­n­dan. Los clientes no suelen pe­r­ca­tar­se de las mejoras en el código, ya que la fu­n­cio­na­li­dad no varía, de forma que la utilidad de la re­fa­c­to­ri­za­ción no es siempre obvia.
La eli­mi­na­ción de las re­du­n­da­n­cias aumenta la efi­cie­n­cia del código. Cuando la re­fa­c­to­ri­za­ción es realizada por equipos grandes, llegar a acuerdos podría suponer más trabajo del esperado.
Los métodos cerrados en sí mismos impiden que los cambios locales afecten a otras partes del código.
Un código bien es­tru­c­tu­ra­do, con métodos y clases más cortas y cerradas en sí mismos, puede ponerse a prueba más fá­ci­l­me­n­te.

En términos generales, es im­po­r­ta­n­te añadir funciones nuevas úni­ca­me­n­te si puede ma­n­te­ne­r­se intacto el código fuente original y, de la misma manera, cambiar el código fuente (es decir, re­fa­c­to­ri­zar) úni­ca­me­n­te si no pueden añadirse funciones nuevas.

Ir al menú principal