Refactoring en Español (y 6)

Hoy, por fin, termino esta serie de artículos basados en el ejemplo del primer capítulo del libro de Fowler “Refactoring“.

Recapitulemos un poco antes de comenzar.

Desde la primera entrega hasta ésta no hemos cambiado el comportamiento del sistema: sólo hemos cambiado el diseño interno. A eso le llamamos refactorizar. (Por cierto, mi siguiente entrega de “Aprender inglés es fácil… si sabes cómo” irá justamente sobre esta palabra).

Y estamos seguros de que no ha cambiado el comportamiento porque tenemos una batería de pruebas unitarias que así nos lo han ido demostrando.

A lo largo de las cinco entregas anteriores hemos ido moviendo código dentro de las clases hacia métodos privados que, además, mejoraban la comprensión del código. También hemos movido código entre clases, delegando responsabilidades entre objetos. Y finalmente, en la última entrega dejamos unas condiciones múltiples (switch) en los métodos getCharge y getFrequentRenterPoints en la clase Movie. Vamos a ver en este final del ejemplo cómo eliminar esos ifs, lo cuál me sirve de entradilla para comentar la campaña anti-if (originalmente en italiano, sí, en italiano).

Comenzamos.

At last… Inheritance

Para no plagiar, creo que lo mejor es recomendar directamente la lectura de este capítulo 1 del libro. Sin embargo, y aunque la explicación es excelente, hay que reconocer que el original está en inglés… 🙂

Aunque no me dedico a la traducción y mi inglés es mejorable, voy a tratar de aportar mi granito de arena a la causa hispana.

AL FIN… HERENCIA

Tenemos varios tipos de películas que tienen diferentes maneras de responder a la misma pregunta. Esto parece un trabajo para subclases. [N.T. El autor se refiere a Movie.getCharge]. Podemos tener 3 subclases de Movie, cada una de las cuáles teniendo su propia versión de getCharge. (Ver Fig. 1.14)

[N.T. La fig. 1.14 muestra un diagrama de clases UML con las subclases RegularMovie, ChildrensMovie y NewReleaseMovie, heredando de Movie. Todas tienen un método llamado getCharge.]

Esto nos permite sustituir la instrucción switch por el uso de polimorfismo. Desgraciadamente esto tiene un pequeño problema: que no funciona. Una película puede cambiar su clasificación a lo largo de su vida. Un objeto no puede cambiar su clase a lo largo de su vida. Sin embargo, hay una solución en el patrón Estado [GoF]. Usando el patrón Estado, nuestro diseño quedaría como en la fig. 1.15

[N.T. La fig. 1.15 muestra otro diagrama de clases UML con la clase Price y sus subclases RegularPrice, ChildrensPrice y NewReleasePrice. Además, también muestra la clase Movie usando una instancia de Price para el cálculo de getCharge.]

Añadiendo la indirección podemos hacer las subclases del objeto Price y cambiar el precio cada vez que lo necesitemos.

Si estáis familiarizados con los patrones del GoF, quizás os preguntéis: “¿Es esto un estado o una estrategia?”. ¿Representa la clase Price a un algoritmo para calcular el precio (en cuyo caso yo preferiría llamarla Pricer o PrincingStrategy) o representa al estado de una película (Star Trek X es una novedad)? En este punto, la elección del patrón (y del nombre) refleja cómo queremos pensar acerca de la estructura. De momento estamos pensando en este objeto como el estado de una película. Si más adelante decidimos que una estragia comunica mejor nuestras intenciones, refactorizaremos cambiando los nombres.

Hasta aquí la traducción. Espero que os ayude a entender el cambio que vamos a introducir en el diseño y el por qué lo hacemos.

Vamos a introducir el patrón Estado. Para ello nos aseguramos de que tenemos bien encapsulados los accesos al atributo Movie._priceCode. (Lo cambiamos incluso en el constructor).

TESTS

Para introducir Price y toda la jerarquía hacemos TDD y, para ello, creamos PriceTest como sigue y lo añadiremos a la suite TestAll.

package step6;

import org.junit.Test;

public class PriceTest {

   
  public void testChildrensPrice() {
    Price aPrice = new ChildrensPrice();
    assertEquals(Movie.CHILDRENS,aPrice.getPriceCode());
  }

}

Lógicamente, como no compila, tendremos que crear las clases y métodos correspondientes. Al ejecutar los tests, salen en rojo (si hemos implementado getPriceCode con un valor por defecto), pero en cuanto hacemos que getPriceCode devuelva el valor esperado según la subclase que vayamos incorporando, volvemos al verde. Siempre primero el test y luego el código.

A continuación introducimos el uso de Price en Movie tal y como propone Fowler. De hecho, al copipegar su implementación de Movie.setPriceCode incorpora código para el que no teníamos pruebas. Esto lo descubrimos al ver el informe de cobertura de nuestras pruebas y comprobar que no tenemos una prueba para comprobar qué pasa cuando asignamos a una película un tipo de precio desconocido. (Sí, en sentido estricto no estaríamos refactorizando puesto que hemos cambiado los tests, e.d. la funcionalidad original, pero yo más bien diría que hemos encontrado un defecto en el código original puesto que no contemplaba una condición excepcional).

@Test(expected=IllegalArgumentException.class)
public void testMovieIllegalType() {
  new Movie(ANY_TITLE,UNKNOWN_PRICE_CODE);
}

Por lo demás, tras ejecutar nuestra batería de tests, el refactor ha ido bien. (Lo sabemos porque nuestros tests están en verde)

Siguiendo los pasos de Fowler, refactorizamos Movie.getCharge para que use un nuevo Price.getCharge (OJO, sin olvidar que estamos haciendo TDD) 🙂

Implementamos Price.getCharge moviendo el código de Movie.getCharge a la clase abstracta (aunque este método no será abstracto).

No eliminamos el viejo test puesto que es lo que nos indicará que funciona o no nuestro refactor; pero implementamos los tests como el siguiente. (En realidad es fácil porque son idénticos a los de Movie)

 
public void testChargeForChildrens() {
  Price aPrice = new ChildrensPrice();
  double charges[] = { 1.5, 1.5, 1.5, 3.0, 4.5, 6.0, 7.5 };
  for (int daysRented = 1; daysRented <= charges.length; daysRented++) { 
    assertEquals(charges[daysRented-1], aPrice.getCharge(daysRented),0);
  }
}

TESTS

Declaramos el método ahora como abstracto e implementamos el primero que nos venga en gana (p.ej. ChildrensPrice.getCharge). Ya tenemos los tests, por lo que iremos refactorizando con seguridad. Recordad el lema de TDD: “RED GREEN REFACTOR“.

TESTS

A continuación, vamos haciendo los cambios correspondientes al resto de tipos de precio. Siempre con pasos seguros gracias a nuestra batería de tests.

TESTS

Y finalmente hacemos lo mismo con getFrequentRenterPoints:

  • movemos el código de Movie.getFrequentRenterPoints a Price.getFrequentRenterPoints (incluyendo primero los nuevos tests porque hacemos TDD)
  • no olvidamos mover los javadocs
  • implementamos el caso particular de NewRelease.getFrequentRenterPoints (dejando el caso general en la clase abstracta Price)

Pues bien, llegados a este punto ya hemos terminado con el ejemplo de Fowler. Os recomiendo encarecidamente el libro (en él se detallan las técnicas de refactor y los “tufillos” (smells) que nos indican cuándo es posible que nuestro código requiera ser refactorizado) y un par de secuelas muy interesantes:

  • Refactoring Workbook, con el que podréis ejercitar todo lo que hayáis aprendido con el de Fowler.
  • Refactoring to Patterns, con el que podréis orientar vuestros refactorajes hacia patrones de diseño, mejorando así la calidad de vuestros diseños.

Finalmente, una última recomendación: http://sourcemaking.com/refactoring

Desgraciadamente todas estas referencias están en inglés. 🙁
¿Cuándo nos decidiremos a crear contenidos en español ahora que estamos reivindicando la profesión aquí en España?

Tagged: