domingo, 25 de septiembre de 2016

Contra los largos métodos

Estos últimos días he estado revisando código para el testeo (no hay tests unitarios) de una aplicación realizada en symfony. He estado tratando de entender qué hace para poder ver si funciona o no.

Bien, en un punto, probando diferentes tipos de alarmas que heredan de una clase madre, he visto que cada una de esas clases tiene un método run. Hasta ahí bien, pero me he encontrado dos cosas que no me han gustado.

  1. El polimorfismo no se usa en una estructura, ni pudiendo pasar distintos objetos cómo parámetros para ejecutar el método a posteriori. Cada método run de cada alarma se ejecuta de forma autónoma en condiciones diferentes nada más instanciar el objeto de cada alarma concreta. 
  2. El método run es el único método de la clase concreta y cada uno tiene entre 200 y 400 líneas.

Evidentemente entender a veces qué hacen esos métodos tan largos me está generando ciertos quebraderos de cabeza, y alguna vez me pierdo y tengo que volver atrás, cómo quien se duerme viendo una película y busca dónde se quedó antes de sucumbir.

Así que he aprovechado para resumir qué dice Martin Fowler en su libro “Refactoring: Improving the design of existing code”.

Recordemos que un método debe hacer una única cosa, y en 300 líneas de método me da que caben bastantes.

Basta decir que lo primero que dice, como antítesis del enunciado “Long Methods”, es que los métodos deben ser cortos. ¿Pero cómo? Ahí viene la parte interesante.

He decidido no meter aquí ejemplos de código para no hacerlo muy largo y que se pueda leer como un simple manual con recomendaciones a seguir. Ya vendrá el código concreto en otros posts.


Extraer métodos.



Lo más básico es que si hay código duplicado dentro del método, o que hace casi lo mismo en varias partes, hay que sacarlo como un método en sí mismo. Esto es así siempre. El código no se duplica.

Hay veces en los que hay un trozo de código que dan ganas de comentar para que se pueda entender, por lo que eso tiene toda la pinta de que se puede extraer en un método. Aquí hay que tener cuidado y no extraer métodos por extraerlos. Por ejemplo, si vas a extraer 2 o 3 líneas (o alguna más) quizás no merezca la pena, o quizás sí. Ahí entra el caso concreto y la valoración del contexto. ¿El método tiene entidad por sí mismo? ¿Tendría sentido sin el método del que se ha extraído?

Los métodos extraídos deben tener buenos nombres. El nombre debe contener la intención del método, no cómo hace lo que hace. Esto es importante. ¿Es difícil elegir el nombre con la intención? Quizás es que tiene más de una intención y no es un único método a extraer. Resumiendo, el nombre del método debe decir lo mismo que el comentario que se iba a añadir, pero sólo en un nombre.


Reemplazar variables temporales con una consulta.



Si al método extraído se le pasan muchos parámetros que no son más que variables temporales, creadas únicamente para contener resultados de ciertas expresiones, tenemos un problema. Muchos parámetros complican entender qué hace el método, y por otra parte estamos acoplando el método extraído a resultados calculados en el largo método que queremos acoplar. No aportamos comportamiento sólido a la clase con ese nuevo método.

Hay que eliminar esos parámetros. IMPORTANTE: Esto aplica a variables temporales que sólo se asignan una vez, ya que vienen de una expresión que ha realizado un cálculo, y dicha expresión no tiene efectos colaterales. Es decir, no modifica el estado del objeto. Un ejemplo típico es el cálculo a base de variables de clase para obtener un valor que se usa temporalmente. Ese cálculo, que no modifica ninguna de las variables de clase, se puede extraer en un método, aportando comportamiento a la clase.


Introducir un objeto con los parámetros.



Vale, tenemos parámetros que no son variables temporales, son variables que se usan a lo largo del método que incluso pueden cambiar durante la ejecución, y encima vemos que habitualmente se pasan de forma conjunta. ¿Cómo procedemos si no queremos pasar una gran cantidad de parámetros al método extraído? Siempre se puede pasar un array como parámetro, pero eso sería sólo en el caso de que tuviésemos pocos parámetros sin ningún tipo de comportamiento. ¿En esos casos merece la pena crear una nueva clase? Lo más probable es que no.

Pero en el caso en el que durante la ejecución del método las variables a pasar puedan cambiar, o sean partícipes de expresiones (duplicidad de código asegurada), lo ideal es extraer esas variables, y su comportamiento, que será más evidente al agruparlas, en una clase para pasar así un objeto como parámetro. Esto facilitará acortar bastante el método y encapsular variables y comportamiento en un único objeto, lo que a la larga favorecerá la comprensión y modificación del código.

Resumiendo, vamos a crear, a base de variables y el comportamiento que conllevan, un objeto que se pasará como parámetro al método extraído.


Pasar el objeto completo.



Bueno, este es muy sencillo, si pasamos cómo parámetros muchas llamadas a métodos de un objeto, o incluso muchas variables de clase del propio método, siempre podemos pasar el objeto completo, aunque este sea el propio objeto desde el que se pasan los parámetros si estamos pasando un montón de variables de clase recuperables con getters.


Extraer un método como un método de otro objeto.



Como ya hemos hecho anteriormente al introducir un objeto con los parámetros, volvemos a crear una nueva clase para reducir código de un método largo. Esta vez hay una diferencia. Se extrae un método y, a partir del nombre de ese método, se crea una clase que se instancia y usa. ¿En qué casos hacemos esto?

Aquí volvemos a encontrarnos variables temporales que se usan en el método original pero, debido a su complejidad, no pueden extraerse con una consulta. La idea a priori es simple (aunque en el fondo compleja); se crea una clase con variables de clase que representan esas variables temporales, se calculan en el constructor, y se crea un método con nombre tipo “compute”, “calculate” o similar. Teniendo ahora ese método aislado podemos aplicar los pasos anteriores para reducirlo, en la nueva clase, en métodos más pequeños.

En resumen, hemos creado un objeto a partir de un método que se instancia para ejecutar el método. Esto, si no se hace con cuidado, puede generar en clases erróneas que no es necesario crear y que realmente no representan una entidad. Otra forma de refacrorizar es eliminar clases que no lo son porque sólo hacen una cosa y no representan ninguna identidad. Este caso, por tanto, hay que hacerlo con mucho cuidado; un ejemplo sería el patrón “strategy pattern” en el cual se extrae comportamiento de una clase para usar una estrategia u otra tirando de composición en vez de herencia.


Descomponer un condicional.



Hemos encontrado una sentencia if-elseif-else muy compleja que no favorece la legibilidad y que, en algunos casos, puede incluso matar unas cuantas neuronas al tratar de seguir su lógica.

Un ejemplo sería un if tipo si es A y no es B y tampoco es C pero tiene que tener D, y todo ello lo podemos resumir en un es E. Creamos un método llamado E que maneja las comprobaciones múltiples, por lo que en el condicional sólo se llama a E dejándolo mucho más claro. Recordemos, el método tiene que tener un nombre que deje totalmente clara la intención del condicional compuesto y dejar el cómo en la implementación del método.