Cuaderno: Refactoring. Malos olores dentro de una clase: Nombres

Introducción

Algunas herramientas para escoger nombres pueden ser:

  • Diccionarios de proyectos.
  • Vocabularios del dominio, ontologías y lenguajes.
  • Metáforas de Xtreme Programming.

Los buenos nombres cumplen varias funciones:

  • Proveen de un vocabulario para discutir nuestro dominio.
  • Comunican intención.
  • Aportan expectativas sobre cómo funciona el sistema.
  • Se apoyan entre sí en un sistema de nombres.

Para elegir buenos nombres, utiliza las siguientes guías:

  • Usa verbos para manipulators (modifican estado) y nombres o adjetivos para accesors (retornan estado).
  • Usa la misma palabra para expresar conceptos similares y palabras distintas para expresar conceptos distintos.
  • Prefiere nombres de una sola palabra.
  • Valora más la comunicación.

Entre los malos olores que podemos encontrar dentro de una clase, aquellos de una gravedad moderada son los relacionados con los nombres:

Type Embedded in Name

Qué hacer

Utiliza Rename Method (o field o constant) sobre un nombre para comunicar intención sin estar tan ligado a un tipo.

Recompensas

  • Mejora la comunicación.
  • Puede facilitar la identificación de duplicidad.

Contraindicaciones

  • Manten el tipo en el nombre cuando tengas una misma clase de operación para distintos tipos relacionados.
  • No elimines el tipo si trabajas bajo un estándar utilizado por el equipo.

Uncommunicative Name

Qué hacer

Utiliza Rename Method (o field, constant, etc.) para darle un mejor nombre.

Recompensas

Mejora la comunicación.

Contraindicaciones

  • El uso de nombres como i/k/j para índices de iteradores o c para caracteres no son muy confusos si el ámbito es pequeño.
  • En ocasiones puedes encontrarte con que las variables enumeradas comunican mejor.

Inconsistent Names

Qué hacer

Elige el mejor nombre y utiliza Rename Method (o field, constant, etc.) para poner el mismo nombre al mismo concepto. Una vez hecho, verás que las clases se ven más similares que antes. Busca entonces olores de duplicidad de código y elimínalos.

Recompensas

  • Mejora la comunicación.
  • Puede exponer duplicidad.

Cuaderno: Refactoring. Malos olores dentro de una clase: Olores leves

Los malos olores leves que podemos encontrar dentro de una clase son:

Comments

Qué hacer

  • Cuando un comentario explica un bloque de código, utiliza Extract Method para encapsular dicho bloque de código y aportar semántica. El comentario suele sugerir un nombre para el método.
  • Cuando un comentario explica qué hace un método mejor que el propio nombre del método, utiliza Rename Method y aprovecha la base del comentario para darle un mejor nombre.
  • Cuando un comentario explica condiciones previas, considera utilizar Introduce Assertion para reemplazar el comentario con código.

Recompensas

  • Mejora la comunicación.
  • Puede exponer duplicidad.

Contraindicaciones

No elimines comentarios que explican la razón de por qué algo es hecho de una manera determinada.

Long Method

Qué hacer

  • Utiliza Extract Method para romper el método en porciones más pequeñas. Busca comentarios y espacios en blanco que separen bloques interesantes. Extrae métodos que tengan significado semántico, no sólo una porción de código al azar.
  • Puedes encontrar otras refactorizaciones (que limpien líneas, condicionales y usos de variables) que sean útiles antes de comenzar a separar el método.

Recompensas

  • Mejora la comunicación.
  • Puede exponer duplicidad.
  • Suele ayudar a la aparición de nuevas clases y abstracciones.

Contraindicaciones

En ocasiones puede que un método largo sea la mejor manera de expresar algo.

Large Class

Qué hacer

  • Utiliza Extract Class si puedes identificar una nueva clase que tiene parte de las responsabilidades de la clase actual.
  • Utiliza Extract Subclass si puedes dividir las responsabilidades entre la clase y una nueva subclase.
  • Utiliza Extract Interface si puedes identificar un subconjunto de características utilizadas por los clientes de la clase.

Recompensas

  • Mejora la comunicación.
  • Puede exponer duplicidad.

Long Parameter List

Qué hacer

  • Si el valor del parámetro puede ser sustituído desde otro objeto que ya lo conoce, utiliza Replace Parameter with Method.
  • Si el parámetro viene de un único objeto, prueba Preserve Whole Object.
  • Si los datos no vienen de un objeto lógico, quizá te interese agruparlos mediante Introduce Parameter Object.

Recompensas

  • Mejora la comunicación.
  • Puede exponer duplicidad.
  • Suele reducir el tamaño.

Contraindicaciones

  • En ocasiones quieres evitar una dependencia entre dos clases.
  • A veces los parámetros no tienen un significado conjunto.

Un novato en refactoring. Parte 3: Hide Delegate y Middle Man, o cómo cumplir con la Ley de Demeter

Sobre la Ley de Demeter

Este es uno de los pocos conceptos de la programación orientada a objetos que debe tomarse como una ley y no como un principio o directriz. Es decir, que debería ser obligatorio en el diseño de nuestra aplicación.

La Ley de Demeter (Law of Demeter en inglés) o LoD, también conocida como Principio del menor conocimiento (Principle of Least Knowledge) tiene como objetivo la reducción del acoplamiento y el incremento de la cohesión.

Enuncia lo siguiente:

  • Cada unidad debe tener un limitado conocimiento sobre otras unidades y solo conocer aquellas unidades estrechamente relacionadas a la unidad actual.
  • Cada unidad debe hablar solo a sus amigos y no hablar con extraños.
  • Solo hablar con sus amigos inmediatos.

Trasladando la Ley de Demeter a código

Para cumplir con dichas premisas a nivel de código, se parte de que un método sólo debería utilizar otros métodos de:

  • La propia clase a la que pertenece.
  • Un objeto creado por el propio método.
  • Un objeto pasado como parámetro de entrada al propio método.
  • Un objeto almacenado como variable a nivel de la propia clase (propiedad).

Un método nunca debería utilizar otro método de un objeto retornado por sus colaboradores.

Veamos ahora cómo estas técnicas de refactoring nos ayudan a cumplir con dicha ley.

Hide Delegate (ocultar delegado)

Es una técnica utilizada para mover características entre clases. Se utiliza cuando nos encontramos con el problema en que un objeto Client hace una llamada a un método de un objeto Delegate, que a su vez es una propiedad u objeto retornado por un objeto Server, de otra clase. Se estaría produciendo un incumplimiento de la Ley de Demeter.

La forma correcta de proceder es crear en Server un método propio que internamente llame y retorne el comportamiento que Client necesita de Delegate. De esta forma, Client no es consciente en ningún momento de la existencia de Delegate y eliminamos así la dependencia entre estas dos clases, reduciendo el acoplamiento en nuestra aplicación. Server pasa a ser lo que se conoce como Middle Man. Un objeto de una clase intermedia que se encarga de comunicar las peticiones de una clase a otra, que es la tiene el comportamiento que buscamos para desarrollar la lógica deseada.

La contra de esta técnica de refactorización es que para cada comportamiento que Client necesite de Delegate, tendremos que crear un método en Server que se encargue de la comunicación entre ambos.

Veamos ahora la técnica opuesta a Hide Delegate.

Remove Middle Man (eliminar intermediario)

Partiendo del ejemplo anterior en Hide Delegate, Client requiere una lógica que se encuentra en Delegate y Server hace de Middle Man entre ambos.

Ahora bien, ya sea que nos encontremos en una etapa temprana del diseño de la aplicación o bien tras un tiempo de evolución en la misma mediante diferentes fases de refactoring, podemos encontrarnos con que un Middle Man carece de comportamiento propio y únicamente se encarga de delegar el trabajo a otras clases. Llegado este punto, debemos estudiar si realmente queremos que exista esta clase intermedia o si la eliminamos para reducir una complejidad innecesaria en el diseño de nuestra aplicación.

Para eliminar el Middle Man, debemos sustituir su dependencia por la de Delegate en la clase Client, cambiando todas las referencias al falso comportamiento del mismo por referencias al comportamiento final correspondiente en Delegate.

Algunos casos en los que podemos querer mantener la existencia de una clase Middle Man son:

  • Si evita una interdependencia entre clases (reducir el acoplamiento)
  • Sirve a la implementación de un patrón de diseño, como el Proxy o el Decorator.

Conclusión

El uso de Hide Delegate y Middle Man permite cumplir con la Ley de Demeter y asegurarnos de que nuestro diseño mantenga una alta cohesión y un menor acoplamiento, si bien un uso excesivo de esta técnica puede resultar en un crecimiento desmesurado, o quizá innecesario, de la complejidad de nuestra aplicación, que podemos revertir mediante Remove Middle Man.

La forma de asegurar que la implementación de ambas técnicas es la mejor solución para cada caso en particular, así como del resto de la arquitectura resultante en nuestro diseño, no es otra que dedicar unas horas semanales a realizar una revisión del análisis de nuestro proyecto, con su correspondiente refactoring de considerarse necesario.

La reescritura habitual del código no sólo ayudará a que el diseño sea o se acerque lo máximo posible a ser la solución ideal en el momento en el que es escrito el código, sino que ayudará a que el mismo evolucione junto con nuestra lógica de negocio y maximizará la longevidad del proyecto.

Un novato en refactoring. Parte 2: Cambios seguros

Los pasos en la refactorización

Las diferentes técnicas que conforman la refactorización tienen un mismo denominador común. Todas se dividen en pequeños pasos en los que se debe tener siempre presente que el cambio realizado en el código no debería dar como resultado un error del mismo a nivel de compilación o interpretación, salvo que esto sea intencionado. Esto puede extraerse de la segunda definición de refactoring citada en mi anterior artículo. En ella, se hace hincapié en el hecho de que los cambios deben realizarse de forma segura.

Un ejemplo de refactoring

Para ilustrar este aspecto, utilizo los pasos que observé en un tutorial online sobre refactoring en el que se realiza la técnica conocida como Replace Temp with Query, la cual se basa en sustituir la declaración y asignación de valor de una variable local por una llamada a otro método que nos retorne dicho valor. Si bien esta práctica puede suponer una ínfima disminución del rendimiento de la aplicación al tener que computar en cada llamada la lógica contenida en el método, aporta mayor claridad a la hora de leer y refleja mejor nuestras intenciones. Partiendo del siguiente código, realizaremos los pasos para sustituir la variable local discountFactor por una llamada a un método accesor con el mismo nombre.

El primer paso me pareció un truco muy interesante. Se trata de añadir el modificador final a la variable discountFactor para indicar que no queremos que su valor sea sustituido en un punto posterior del método, lo cual es clave para sacarle partido a Replace Temp with Query. De esta forma, nuestro IDE se encargará de notificarnos que hay un error en la compilación (intencionado por nuestra parte) si el valor de la variable es modificado en un punto posterior.

Una vez identificada la porción de código a refactorizar, la extraemos a un nuevo método con el mismo nombre que la variable en cuestión. Podemos ayudarnos del atajo Refactor > Extract method de nuestro IDE. En el caso de no contar con uno, primero copiamos la lógica a extraer al nuevo método, asignamos la llamada al método como valor de la variable y borramos la porción de código extraído.

Finalmente, utilizamos para el cálculo la llamada al nuevo método en lugar de la variable y la eliminamos.

El código se ha mantenido estable durante todo el proceso de refactorización, a excepción del uso de final en la variable. Esta sería la forma correcta de proceder siempre que refactorizamos.

Curiosamente, en el tutorial en el que se explicaba esta técnica, el código quedaba roto por un momento en el último paso descrito anteriormente. Al realizar manualmente la extracción de la lógica a un nuevo método, el autor eliminaba la declaración de la variable antes de sustituir su uso en el cálculo por la llamada al nuevo método. Si se intentase compilar el programa en ese punto, obtendríamos un error al no estar declarada la variable discountFactor.

Conclusión

Durante el refactoring no debemos olvidar que la aplicación, salvo que así lo queramos, nunca debe conducir a un fallo tras un paso en la modificación del código.

Este es un factor realmente importante, más aún cuando el proceso de refactorización que estemos realizando alcance un mayor nivel de abstracción. Es decir, que conlleve cambios externos a una clase, relacionados con la forma en que estas se comunican entre sí.

Un novato en refactoring. Parte 1: Introducción

Este es el primero de una serie de artículos en los que voy a tratar sobre mis experiencias con la refactorización (refactoring en inglés), siendo un novato en proceso de aprendizaje sobre dicha materia, mientras avanzo en mi lectura del libro Refactoring Workbook, por William C. Wake.

Comienzo este artículo por el que fue mi primer encuentro con este tema: La definición. Más concretamente, la que puedes encontrar en este artículo de Wikipedia.

La primera definición (no tan buena)

“La refactorización es una técnica de la ingeniería de software para reestructurar un código fuente, alterando su estructura interna sin cambiar su comportamiento externo.”

Wikipedia

Estas fueron las primeras palabras con las que me transmitieron (o intentaron transmitir) el significado de refactoring y al que mi mente contestó con un seco e indiferente OK como el de cierto personaje ficticio.

La definición es correcta desde el punto de vista de un formador, alguien que ya posee este conocimiento que se quiere hacer llegar a quien la lee. Sin embargo, estas palabras no transmiten nada más allá de lo que meramente expresan. Es decir, de ella podemos extraer:

  • Técnica (La acción)
  • Ingeniería de software (El ámbito)
  • Reestructurar un código (El qué)
  • Sin cambiar su comportamiento (El cómo)

Si nos paramos a pensar un momento y somos capaces de ponernos en el punto de vista de un aprendiz, seremos capaces de detectar en ella la ausencia de las dos partes que de verdad importan, las más esenciales para transmitir el concepto. Estas son el por qué y el para qué.

Definiciones con una estructura similar a esta están presentes en todos los ámbitos y se llevan escribiendo y repitiendo durante décadas. Fracasan miserablemente en el cumplimiento de su objetivo, carecen de la capacidad de plasmar el significado que pretenden comunicar y por ende el sentido mismo de su existencia.

Además, en su esfuerzo de utilizar un vocabulario técnico se vuelven frías y vacuas. Características que hacen parecer como si no hubiesen sido escritas para ser leídas y reflexionadas por otro ser humano. Sobretodo si aún está aprendiendo sobre su ámbito.

La segunda definición (mucho mejor)

Veamos ahora por el contrario, como es descrito el refactoring en la siguiente definición, extraída del libro citado en el inicio de este artículo.

“La refactorización es el arte de mejorar el diseño de un código existente de una manera segura. Nos provee de formas para organizar un código problemático y nos da las pautas para mejorarlo.”

William C. Wake

A diferencia de la primera definición, esta se centra completamente en cumplir su objetivo de transmitir el concepto al que se refiere.

De ella, seguimos extrayendo:

  • La acción, de una forma quizá más acertada, enfocada a la artesanía en “el arte”.
  • El ámbito, en “el diseño de un código”.
  • El qué, en “organizar un código”.
  • El cómo, en “de una forma segura”.

Y finalmente:

  • El por qué, en “código problemático”.
  • El para qué, en “mejorar el diseño”.

Conclusión

Siempre es útil consultar distintas fuentes sobre una materia en concreto, pues te ayudarán a despejar las posibles dudas que te puedan surgir de dar con una única explicación incompleta.

El fin de la refactorización es la mejora en el diseño del código existente.