Post invitado: "Refactoring" por Quique Martínez

¿Qué es el “refactoring”?

La refactorización (“refactoring”) es una técnica de ingeniería de software para la restructuración del código fuente. Esto nos permite dejarlo más claro y fácil de mantener.

Básicamente se puede decir que se trata de modificar la estructura interna del código sin modificar el comportamiento.

El proceso de la refactorización pasa por pequeños cambios como renombrar variables, simplificar el código más complicado o borrar lógica duplicada.

Se tiene que tener cuidado porque después de realizar una refactorización es posible que alguna parte del código deje de funcionar como lo hacía. Es por ello muy recomendable tener una buena cobertura de test, que nos asegurarán que todo sigue funcionando correctamente. Este proceso nos puede llevar desde minutos a meses. Ya que no solo se trata de borrar código duplicado o renombrar variables, también se puede hacer una refactorización de toda la arquitectura (si es necesario) con lo que implicaría realizar cambios en múltiples componentes de nuestra solución.

¿Por qué es importante?

Son muchos los motivos para realizar una refactorización de nuestro código, a continuación detallamos los más comunes.

  • Un código más compresible y fácil de mantener
  • Facilidad para agregar nueva funcionalidad
  • Mejora en la calidad del código existente

A nosotros o nuestros compañeros no nos gusta encontrarnos clases con cientos de líneas, lógica duplicada, código que no se utiliza o variables con nombres que no sabemos a qué se refieren.

Todo esto hace nuestro trabajo más difícil y tedioso.

¿Cuantas veces no hemos pensado en hacer vudú contra algún compañero?

Bromas aparte tenemos que pensar que el código que estamos creando durante nuestro día a día y que entendemos a la perfección en ese momento lo tendrá que modificar alguien más tarde (O nosotros mismos). Horas, días, meses o incluso años después. Y en ese momento no se lo estaremos poniendo fácil. Es por ello que es muy importante el crear un código limpio, fácil de comprender y mantener.

Todos hemos programado con prisa, durante la noche y con mucha presión y hemos generado líneas y líneas de código. O simplemente estamos en un proyecto que lleva mucho tiempo desarrollándose y la lógica ha crecido tanto que es necesario simplificarla. Ya que cada nueva funcionalidad nos está costando implementarla cada vez más. Por ello la refactorización nos va ayudar en nuestro día a día y va aumentar nuestro rendimiento y el del resto del equipo.

Además de todas estas ventajas, podremos ir viendo como la calidad del código de nuestro desarrollo ha ido mejorando. Ya que al realizar las refactorizaciones vamos a ir encontrado diferentes “Code smells”y los iremos corrigiendo, dejando así un código de mayor calidad.

Refactorizando nuestros problemas en nuestro día a día

Algunos de los problemas más comunes que nos encontramos en nuestro día a día suelen ser los siguientes:

Variables con nombres genéricos o imposibles de saber a qué hacen referencia

Muchas veces nos encontramos variables del estilo: “MyProperty” o simplemente “m” la primera haciendo referencia a una propiedad pero con un nombre muy genérico y la segunda a priori no sabemos para que la utilizamos.

Una posible refactorización que nos va a ayudar a entender más el código va a ser renombrarlas. Vamos a intentar que el nombre de las propiedades y variables sean lo más ajustadas a lo que van guardar. Es decir si queremos guardar los minutos de una hora en una variable no utilicemos “m” sino “minutos”. De esta forma a primera vista vamos a poder saber para que la usamos.

Ya tenemos la teoría veamos ahora la práctica.

Si trabajáis como nosotros con Visual Studio tenéis una serie de herramientas que nos ayudan con la refactorización diaria.

Veamos como renombrar el nombre de una variable, propiedad o función:

Nos permite hacer un renombrado seleccionando la variable en cuestión.

Únicamente tenemos que escribir el nuevo nombre y el solo hará el renombrado, ahorrando nos un valioso tiempo

De esta forma podemos ir dejando más claro nuestro código ya que si vamos renombrando las variables, funciones y propiedades por nombres que a simple vista nos den más pistas lo entenderemos mejor nosotros y el resto del equipo.

Código Zombie o que no se utiliza

Otro de los problemas más comunes que nos encontramos es código comentado o que directamente no se utiliza. Esto hace que tengamos clases con cientos de líneas de código y se nos haga muy pesado el trabajar con ellas. En este caso lo que tenemos que hacer es directamente eliminar todo el código comentado y que no se usa. No tengamos miedo, ya que tenemos un repositorio de código fuente. En caso de que no dispongáis de uno, recordad que tenéis Team Foundation Service, que entre otras muchas funciones tiene la de repositorio de código fuente. Además tiene una versión gratuita.

Ahora ya sabemos que tenemos que borrar todo este código, en el caso del código comentado es sencillo. Veamos como eliminar el código que no se utiliza y solo nos ocupa espacio.

En Visual Studio lo tenemos muy sencillo, mirar una clase de ejemplo:

A simple vista y gracias al IDE, sé que el método Subtraction no se utiliza ya que tiene 0 referencias y sé que el método Sum se utiliza en una ocasión. (Aquí lo vemos a simple vista pero lo normal sería que Sum estuviera en otra clase.)

Si pulsamos en esa referencia:

Podemos ver exactamente donde se está utilizando. Esta herramienta nos va a permitir el poder borrar código que no utilizamos y tener así una clase más limpia. También nos servirá con la propiedades, muchas veces declaramos propiedades que no acabamos utilizando, gracias a esto las podremos detectar a simple vista.

Refactoring to Patterns (patrones de refactorización)

Existen muchos patrones que nos ayudan en nuestras refactorizaciones, algunos de ellos los utilizamos a diario sin saber que son patrones como tal.

A continuación veremos un par de ejemplos

Chain Constructors (Constructores encadenados)

Escenario:

Muchas veces al hacer sobrecarga en los constructores de una clase acabamos duplicando código dentro de ellos. Un ejemplo es en la asignación de variables, al hacer la sobrecarga los constructores no reciben los mismos parámetros pero si hacemos las mismas asignaciones dentro de ellos. Otro ejemplo podría ser cuando llamamos a funciones dentro del constructor para calcular algún dato. Si el cálculo lo tenemos que hacer siempre, nos vamos a encontrar esa lógica tantas veces como constructores tengamos.

Veamos cómo podemos refactorizar el código en estos casos.

Ejemplo:

Vamos a utilizar una clase que llamada “Book” con unas cuantas propiedades.

 namespace ChainConstructorsBefore.Model
{
    using System;

    public class Book
    {
        public string Author { get; set; }
        public string CoAuthor { get; set; }
        public string ISBN { get; set; }
        public DateTime PublishDate { get; set; }
        public string PublishingHouse { get; set; }
        public double Price { get; set; }

        public Book(string author, string coAuthor, string isbn, DateTime publishDate, string publishingHouse, double price)
        {
            Author = author;
            CoAuthor = coAuthor;
            PublishDate = publishDate;
            PublishingHouse = publishingHouse;
            ISBN = isbn;
            Price = price * 1.18 * 3.2;
        }

        public Book(string isbn, DateTime publishDate, string publishingHouse, double price)
        {            
            PublishDate = publishDate;
            PublishingHouse = publishingHouse;
            ISBN = isbn;
            Price = price * 1.18 * 3.2;
        }

        public Book(string author, string isbn, DateTime publishDate, string publishingHouse, double price)
        {
            Author = author;            
            PublishDate = publishDate;
            PublishingHouse = publishingHouse;
            ISBN = isbn;
            Price = price * 1.18 * 3.2;
        }
    }
}

Como podéis ver tenemos propiedades y una serie de constructores sobrecargados. En ninguna ocasión reciben los mismos parámetros (aunque he de decir que me lo he encontrado alguna vez) pero si se repite en alguna ocasión el código de asignación de “Author”,”PublishDate”,”ISBN”, “Price” y “PublishingHouse”. Además tenemos el cálculo del precio repetido en cada constructor. Este es un ejemplo sencillo, sé que de primeras podemos pensar no pasa nada por tener las asignaciones repetidas y demás. Pero imaginarlo en vuestro proyecto actual, con muchas clases, más propiedades y demás

Solución

Veamos ahora como refactorizamos este código para que quede más limpio y entendible.

Lo primero que vamos a hacer es extraer el cálculo del precio a una función, así no tendremos la misma lógica repetida en cada constructor.

         private const double BenefitsFactor = 3.2;

        private const double Tax = 1.18;

        public double CalculatedPrice(double price)
        {
            return price * BenefitsFactor * Tax;
        }

Ahora, ya tenemos la función separada, como podéis ver hemos quitado las dos cifras para hacer el cálculo y hemos creado 2 constantes. La práctica de dejar números fijados en las funciones es mala. Por eso creamos las constantes. (Magic Numbers).

Segundo paso, tenemos que refactorizar la asignación de las propiedades. Aquí es donde viene la utilización del patrón.

     public class Book
    {
 private const double BenefitsFactor = 3.2;

        private const double Tax = 1.18;

        public string Author { get; set; }
        public string CoAuthor { get; set; }
        public string ISBN { get; set; }
        public DateTime PublishDate { get; set; }
        public string PublishingHouse { get; set; }
        public double Price { get; set; }

        public Book(string author, string coAuthor, string isbn, DateTime publishDate, string publishingHouse, double price)
        {
            Author = author;
            CoAuthor = coAuthor;
            PublishDate = publishDate;
            PublishingHouse = publishingHouse;
            ISBN = isbn;
            Price = CalculatedPrice(price);
        }

        public Book(string isbn, DateTime publishDate, string publishingHouse, double price)
            : this(null, null, isbn, publishDate, publishingHouse, price)
        {
        }

        public Book(string author, string isbn, DateTime publishDate, string publishingHouse, double price)
            : this(author, null, isbn, publishDate, publishingHouse, price)
        {           
        }

        public double CalculatedPrice(double price)
        {
            return price * BenefitsFactor * Tax;
        }

Como podéis ver ahora únicamente tenemos las asignaciones y la llamada al cálculo del precio en un constructor y seguimos teniendo todas las sobrecargas que hemos creído oportunas.

Compose Method (Crear métodos)

El segundo patrón para realizar la refactorización que vamos a explicar es uno de simplificación.

Muchas veces después la lógica de nuestro desarrollo crece tanto que cuando tenemos que agregar una nueva funcionalidad se nos hace muy complicado. Este patrón trata sobre la refactorización de métodos, haciéndolos más sencillos y fáciles de entender.

Este patrón va muy ligado a la “S” de S.O.L.I.D, que en resumen nos dice que tenemos que crear métodos que contengan solo una responsabilidad.

Escenario

Imaginaros que tenemos una aplicación muy simple que agrega nombres a una lista de espera. Para poder agregar estos nombres, tendremos que comprobar antes si la persona ya está en dicha lista. Seguramente a bote pronto, ya lo veis muy claro. Pero pongámonos en el peor de los casos.

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ComposeMethodModel
{
    public class Person
    {        
        public string Name { get; set; }
        public DateTime Date { get; set; }
                                      
        public bool Add(Person person)
        {
            int i = 0;
            bool flag = false;
            while (i < SampleData.Persons.Count())
            {
                var toLowerName = person.Name.ToLower();                

                if (SampleData.Persons[i].Name.ToLower() != toLowerName)
                {
                    i++;
                }
                else
                {
                    flag = true;
                    i = SampleData.Persons.Count();
                }
            }

            if (flag != true)
            {
                var toLowerName = person.Name.ToLower();                

                SampleData.Persons.Add(new Person { Name = toLowerName, Date = person.Date });
                return true;
            }
            return false;
        }

    }
}

Este código funciona perfectamente (y no es el más complicado que hemos visto verdad :P) pero nos tenemos que fijar para saber qué hace.

Lo primero que vemos es que realiza muchas funciones:

  • Recorre la lista de personas
  • Compara la persona que vamos a añadir con todas las existentes
  • En caso de que no exista añade la persona a la lista

Antes de ver como realizaríamos la refactorización, veamos que para este proyecto tenemos un par de Test Unitarios que realizan las siguientes comprobaciones del funcionamiento.

  • Añadir si existe: Nos tiene que devolver un “false” ya que el nombre ya está en la lista.
  • Añadir si no existe: Nos tiene que devolver un “true” ya que hemos agregado el nombre a la lista.
 using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ComposeMethodModel;

namespace UnitTestComposeMethod
{
    [TestClass]
    public class AddTest
    {
        [TestMethod]
        public void AddIfExist()
        {

            bool expectedResult = false;
            Person person = new Person();
            var actualResult = person.Add(new Person { Name = "ExistName", Date = DateTime.Now });
            Assert.AreEqual<bool>(expectedResult, actualResult);
        }

        [TestMethod]
        public void AddIfNotExist()
        {
            bool expectedResult = true;
            Person person = new Person();

            var actualResult = person.Add(new Person { Name = "NotExistName", Date = DateTime.Now });
            Assert.AreEqual<bool>(expectedResult, actualResult);
        }
    }
}

Si ejecutamos los Test con el código actual, veremos que pasan los dos.

Ahora veamos cómo podemos refactorizar la función “Add”.

Lo primero que haremos será extraer la lógica de buscar a una nueva función:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ComposeMethodModel
{
    public class Person
    {
        public string Name { get; set; }
        public DateTime Date { get; set; }

        public bool Add(Person person)
        {
            int i = 0;
            bool flag = ExistName(person);


            if (flag != true)
            {
                var toLowerName = person.Name.ToLower();

                SampleData.Persons.Add(new Person { Name = toLowerName, Date = person.Date });
                return true;
            }
            return false;
        }

        private bool ExistName(Person person)
        {
            int i = 0;
            while (i < SampleData.Persons.Count())
            {
                var toLowerName = person.Name.ToLower();

                if (SampleData.Persons[i].Name.ToLower() != toLowerName)
                {
                    i++;
                }
                else
                {
                    return true;
                }
            }
            return false;
        }

    }
}

Ahora realizaremos la refactorización de la función “ExistName”.

 private bool ExistName(string name)
{
    return SampleData.Persons.Where(p => p.Name == name).Any();
}

Como habéis visto hemos substituido la comparación de strings dentro del bucle, ahora utilizamos LINQ;

Ahora vamos a por la función “Add”.

 public bool Add(Person person)
{
    if (!ExistName(person.Name))
    {
        SampleData.Persons.Add(person);
        return true;
    }
    return false;
}

Como habéis podido ver también hemos quitado la lógica de “ToLowerCase”, ya que no es una buena práctica hacerlo así. Ya tenemos la refactorización acabada. En esta ocasión era muy simple, pero quedaros con la idea de simplificar las funciones. Además recordad, que ahora debemos volver a ejecutar los Test Unitarios y todos deberían pasar en verde :D

Conclusiones

Como hemos podido ver durante este artículo, la refactorización es muy importante ya que nos va a permitir:

  • Un código más compresible y fácil de mantener
  • Facilidad para agregar nueva funcionalidad
  • Mejora en la calidad del código existente

Existen muchos más patrones y os invitamos a conocerlos, al final del artículo encontrareis las referencias y la solución que he utilizado.

Son ejemplos muy sencillos para que os hagáis una idea de que es la refactorización.

Además iréis viendo que cada vez tardareis menos en realizar las refactorizaciones más cotidianas y que el código de vuestras soluciones cada vez tendrá una mayor calidad.

Ahora solo queda que lo probéis en vuestro día a día

Happy coding

Referencias

Refactoring to patterns by Joshua Kerievsky (https://www.amazon.com/Refactoring-Patterns-Joshua-Kerievsky/dp/0321213351)

Refactoring by Martin Fowler (https://www.amazon.com/gp/product/0201485672?ie=UTF8&tag=martinfowlerc-20&linkCode=as2&camp=1789&creative=9325&creativeASIN=0201485672)

Descarga de código

Puedes descargarte el código fuente usado en el post en: https://sdrv.ms/13D7xb4

Quique Martínez

@quiqu3