Rafactorización Orientada al Aspecto

La refactorización, un proceso y un conjunto de técnicas para reorganizar el código preservando su comportamiento externo, ha ganado popularidad debido a su valor práctico en la creación de código ágil. Recientemente la Programación Orientada al Aspecto (AOP) ha recibido una atención creciente debido a su poder de encapsular crosscutting concerts (a lo largo de estas páginas utilizaremos el término crosscutting concerts para referirnos a las "propiedades de un sistema que tienden a estar presentes en varios componentes funcionales") en un sistema a través del uso de una nueva unidad de modularidad llamada aspecto. La refactorización orientada al aspecto igualmente combina estas dos técnicas para refactorizar elementos redundantes. En esta dos páginas, exminaremos los fundamentos de la refactorización orientada al aspecto, los pasos implicados en el proceso, y unas cuantas técnicas de las más comunes,

La Refactorización Orientada al Aspecto (Refactorización AO) va más allá de las técnicas de refactorización convencionales. Aunque algunos pasos de la refactorización convencional modularizan el código en una implementación límpia de OO (Orientada a Objetos), el uso de AOP exprime el códido que no puede ser refactorizado. La Refactorización Orientada al Aspecto ofrece mejoras substanciales en gran variedad de situaciones como en las políticas de manejo de excepciones, el refuerzo de contratos locales, el control de concurrencia y la creación de objetos worker.

La Refactorización AO ofrece muchos beneficios. Inicialmente, su parte más atractiva es la implementación de la misma funcionalidad con menor número de líneas. Sin embargo, después de utilizarla durante algun tiempo muestra su benefico real en que el código es fácilmente entendible, altamente consistente y sencillo de modificar.

A través de estas páginas, utilizaremos AspectJ para las ilustraciones. Sin embargo, la mayoría de las técnicas se pueden exportar a otros sistemas de AOP como AspectWerkz y JBoss/AOP.

. Los Fundamentos

La Refactorización AO proporciona un significado adicional a las técnicas de refactorización convencionales. Consideremos el "Extract method" para refactorización, que encapsula la lógica en un método separado, mientras deja las llamadas a ese método en múltiples sitios potenciales. Con la técnica "Extract method calls", de la Refactorizacion AO podemos dar un paso adicional y refactorizar incluso esas llamadas en un aspect separado. La Refactorización AO también ofrece unas cuantas técnicas por sí misma. Por ejemplo "Extract exception handling" refactoriza los bloques try/catch y cualquier código de manejo de excepciones en un aspect separado, en el proceso de ahorrar una significante cantidad de código así como de crear un código altamente consistente y manejable.

Consideremos un ejemplo concreto de una técnica de Refactorización AO llamada "Extract method calls". Encapsula la funcionalidad cruzada aislando las llamadas a métodos en un aspecto separado. Hay muchas ocasiones para dicha refactorización: log a nivel de clase, chequeo de permisos de seguridad, control de persistencia de la sesión (como las llamadas a openSession() y a closeSession() cuando se utiliza Hibernate). La siguiente figura muestra cómo "Extract method calls" mejora la técnica "Extract method":

En la figura anterior, la implementación inicial tiene un trozo de código duplicado en varias partes. La refactorización "Extract method" encapsula la lógia duplicada en un nuevo método y reemplaza cada trozo de código original con una llamada al nuevo método. Luego, la Refactorización AO de "Extract method calls" encapsula esas llamadas a método en un aspecto. El aspecto contiene un pointcut para capturar todos los lugares donde se debería llamar al método y avisa a ese pointcut de cuando llamar al método refactorizado. El beneficio más importante es la encapsulación de la funcionalidad en el aspecto refactorizado.

La Refactorización AO es más util en crosscutting concerts. En el contexto de AOP, los crosscutting concerts del sistema que se expanden en varias clases y paquetes y reciben la principal atención. Sin embargo, hay crosscutting concerts que afectan a un ámbito pequeño, como aquellos que se aplican sólo a unos pocos métodos de una clase. Muchos de los micro crosscutting concerts a nivel de clase, tratan sobre la inicialización de recursos, y el manejo de excepciones específicos de la clase que son candidatos para la Refactorización AO. También puedes aplicar Refactorización AO en situaciones donde el crosscutting concerts que implica a todo el sistema se aplica restrictivamente a unas pocas clases. De forma frecuente, los aspectos de refactorización que empiezan con una aproximación restrictiva crecen hasta hacerse ampliamente aplicables.

Aunque el beneficio más visible de la Refactorización AO viene cuando hay múltiples localizaciones de un crosscutting concerts, es útil incluso en casos de una única localización para encapsular funcionalidades no-importantes. Esta situación es como cualquier otra en la refactorización tradicional. Por ejemplo, cuando extraes un método incluso aunque sea el único lugar donde se hace una llamada a ese método, obtienes una mejora en el entendimiento del código. Con el tiempo, encontrarás que hay muchos lugares que requieren una llamada para extraer un método. Similarmente, en Refactorización AO, te darás cuenta que los puntos de unión que necesitan la funcionalidad se multiplican. En cualquier caso, la Refactorización AO localiza problemas de funcionalidad como el refuerzo de contratos y el chequeo de acceso en la implementación principal.

Nota:
La relación entre refactorización y AOP es mucho más profunda. Primero, los desarrolladores pueden introducir aspectos más fácilmente en sistemas donde los elementos del programa (clases, métodos, etc.) llevan a cabo una responsabilidad simple y bien definida. Teorícamente, los sistemas con dichas características se podrían crear durante la implementación inicial, aunque en la práctica, son un resultado de los esfuerzos de refactorización. Segundo, muchas técnicas de refactorización convencionales y adicionales (como extract pointcut y extract base aspect) también se pueden aplicar a los aspectos. Sin embargo, ninguna de estas situaciones es el foco de este artículo.

Como la Refactorización AO consiste realmente en un conjunto adicional de técnicas de refactorización, dichos principios de refactorización hacen los cambios en pequeños pasos, utilizan tests de prueba, y los ejecutan antes y después de hacer los cambios, para comprobar que se aplican exactamente como se ha pre-escrito.

. Peculiaridades de la Refactorización AO

Aunque la mayoría de los principios conocidos de AOP se continúan aplicando a la Refactorización AO, unos pocos de esos principios adquieren una importancia especial o una perspectiva diferente en Refactorización AO. Consideremos esos principios:

  • Aproximación hacia la funcionalidad de los Crosscutting Concerts:
    Las discusiones AOP normalmente se enfocan en añadir una funcionalidad de crosscutting concert a una aplicación existente. En la práctica, el proceso recomendado es primero diseñar e incluso prototipar una solución convencional. Luego puedes diseñar e implementar aspectos para encapsular esa funcionalidad. La aproximación a la refactorización es similar. Cuando ya tienes una solución convencional que funciona, la Refactorización AO te ayuda a encapsular la funcionalidad implementada.
  • Aplicabilidad del aspecto al crosscutting concert:
    Cuando se implementa la funcionalidad utilizando aspectos, una aproximación pragmática clama por restringir el crosscutting concert a unas partes seleccionadas del sistema. De esta forma limitamos el efecto de esos aspectos. Luego podríamos incrementar poco a poco el ámbito de los aspectos, algunas veces apuntando a toda la aplicación no restringida. El uso de la refactorización pone un énfasis extra en esta aproximación. La refactorización de aspectos limita deliberadamente los crosscutting concerts a sólo unos métodos o una clase, y algunas veces, a un paquete. Los puntos de corte basados en léxico como within() y withincode() son muy manejables para imponer las restricciones deseadas. Aunque las técnicas de Refactorización AO empiezan con una aproximación muy restrictiva, tienen mucho potencial para toda una aplicación.
  • Consideraciones de acoplamiento:
    En toda la utilización de AOP, es deseable minimizar el acoplamiento entre los aspectos y las clases. Sin embargo, en la utilización de la refactorización, los problemas de acoplamiento reciben un énfasis menor. La refactorización de un aspecto forma parte de la implementación de la clase y por lo tanto podría utilizar un conocimiento íntimo de la clase (como nombres de variables específicos o un lista de métodos explícita). Por supuesto, si hay alguna forma de minimizar el acoplamiento, siempre es deseable su utilización.
  • Situación de los Aspectos:
    En la utilización de la refactorización, como los aspectos podrían tener que cambiar con la implementación, es deseable ponerlos cerca del módulo objetivo. Por ejemplo, si el objetivo de la refactorización es una clase, podríamos implementar los aspectos de refactorización en el mismo fichero fuente, e incluso como aspectos anidados. Aunque en el uso de la AOP se emplean aspectos anidados; en la refactorización consiguen un énfasis adicional.

En suma, la Refactorización AO se construye sobre los principios y prácticas más comunes de AOP, con cambios en el énfasis de algunos pocos. Como podrías esperar, con cualquier tipo de utilización, la AOP continúa ofreciendo beneficios como una mejor modularización, aumentar la comprehensión del código y mejorar su mantenimiento.

. Un Ejemplo: Refactorizar la Extracción de Llamadas a un Método.

Examinaremos el proceso de la Refactorización AO a través de un ejemplo de unas de las técnicas más simples de "Extract method calls". Utilizaremos una clase sencilla con unos pocos métodos ya que con la refactorización como objetivo nos permitirá enfocarnos en el proceso. Este ejemplo muestra como utilizar de forma efectiva las herramientas disponibles para la Refactorización AO hasta que los IDEs proporcionen soporte automátizado, igual que lo han hecho para la refactorización convencional.

Ten en mente, que debido a la extrema simpleza del ejemplo y a la técnica de refactorización utilizada, algunos pasos podrían parecer inútiles, pero son muy útiles en sistemas reales. Por la misma razón, no verás mucho ahorro de líneas de código. En la página siguiente veremos técnicas como "Extract exception handling" que ofrecen un ahorro substancial en líneas de código. El objetivo de está página es presentar el sabor y el proceso de la Refactorización AO.

Consideremos la implementación de la clase Account del siguiente listado que realiza un chequeo de permisos al principio de todos los métodos. Tomaremos este código como punto de partida y a través algunas iteraciones llegaremos a un código bien-refactorizado.

Como puedes ver, no hay mucho que dejar para que una técnica convencional pueda refactorizar los chequeos de permisos: en casi todos los métodos hay una llamada al método checkPermission().

Para la refactorización "Extract method call", divideremos el proceso de factorización en dos pasos obligatorios seguidos por otros dos pasos opcionales. Para otros tipos de refactorización, los pasos variarán un poco, pero el estilo será el mismo. Al final de cada paso, el comportamiento debería ser igual que el del código original, y por lo tanto, deberíamos ejecutar unidades de test para asegurarnos de que la refactorización no ha cambiado su comportamiento - después de todo, esto sobresable en cualquier esfuerzo de refactorización.

. Paso 1: Introducir un Aspecto de Refactorización No-Operacional

El propósito de este paso es crear la infraestructura requerida (crosscutting concert estático y dinámico), pero sin añadir funcionalidad al problema. En esencia, estámos limitados a declarar errores y a declarar avisos y consejos sin ejecutar ningún código adicional.

El primer paso se puede dividir en tres sub-pasos:

  • Insertar un aspecto vacío:
    Insertamos un aspecto vacío anidado que eventualmente implementará un chequeo de permisos para la clase Account. Observa que el uso de un aspecto anidado (o un aspecto emparejado en el mismo fichero fuente) simplifica el seguimiento de los cambios en el código refactorizado. El aspecto se parece a esto:
  • Definir un pointcut para capturar los puntos de unión que necesitan la funcionalidad refactorizada:
    Aquí hemos creado un pointcut que captura todos los puntos de unión donde nos gustaría añadir la funcionalidad refactorizada. Para minimizar los efectos no deseados, el pointcut simplemente enumera todos los métodos necesarios. Aquí podemos ver la definición del pointcut:

    Aunque hemos utilizado un pointcut indivual totalmente especificado, podriamos elegir la omisión de algunas partes (como las excepciones declaradas en cada método) o reemplazar alguna parte con comodines (utilización de ".." en lugar de especificar todos los argumentos). El uso de within() se asegura de que el pointcut no captura ningún punto de unión fuera de la clase Account. En nuestro ejemplo, el uso de within() es redundante y sólo sirve para enfatizar el ámbito del crosscutting concert.
  • Crear un advice no-operacional para el pointcut:
    Dependiendo de la posición necesaria de la lógica del crosscutting concert, utilizamos una forma de advice (consejo) apropiado (before, after, after returning, after throwing, o around). Observa que si elcódigo del crosscutting concert requiere ejecutar código en varias posiciones con respecto a los puntos de unión, necesitaremos más de un consejo, por ejemplo un consejo before y un consejo after. Si utilizas before o after, deja el cuerpo vacío y en el caso de utilizar un consejo around, sólo añade una sentencia proceed() dentro del cuerpo.
    Como en nuestro caso necesitamos ejecutar el chequeo de acceso antes de la ejecución de la lógica de negocio, un consejo before es la elección apropiada.

Aquí está el código fuente después de implementar este paso:

Ahora examinemos si hemos capturado correctamente todos los puntos de unión requeridos. Podemos utilizar el soporte integrado de AspectJ en los IDE’s para hacer más sencilla la inspección visual. La siguiente figura muestra una vista del código en Eclipse:

Como muestra la figura anterior, puedes ver todos los sitios donde se aplica el consejo. Podrías pulsar en todos los métodos aconsejados para ver si de hecho ese sitio tiene una llamada al método refactorizado. Hasta ahora, no hemos modificado nada de la funcionalidad.

. Paso 2: Introducir la Funcionalidad del Crosscutting

En este paso, finalmente moveremos código de la clase principal al aspecto. También realizaremos algún trabajo adicional para ayudar en la implementación correcta y nos acordaremos de refactorizar el aspecto en el futuro.

  • Introducir avisos en tiempo de compilacion (Opcional):
    Este paso recomendado utiliza la facilidad de AspectJ de avisos en tiempo de compilación especificables por el usuario para expresar nuestra intención de refactorizar el aspecto - y no la clase - y hacer las llamadas necesarias. Además, dicha construcción lanzará avisos en caso de que un programador, despreocupado por el aspecto, introduzca de nuevo el método refactorizado en la clase.

    Observa que querrás dejar la sentencia declare warning para avisar a algún nuevo desarrollador que no debería haber ningún método checkPermission() en ningún lugar de la clase.
  • Anadir Funcionalidad crosscutting al Consejo:
    Ahora añadiremos las llamadas de métodos necesarias en el advice. En nuestro caso, añadiremos llamadas a AccessController.checkPermission():

  • Eliminar las llamadas a métodos de los métodos aconsejados:
    Como el advice realiza el chequeo de permisos, necesitamos eliminar las llamada de cada uno de los métodos aconsejados. Por lo tanto, es hora de eliminar esos chequeos de todos los métodos. Puedes utilizar la vista del outline del IDE para encontrar todos los sitios capturados por el pointcut y simplemente eliminar las llamadas. Si te olvidas de algún método, la sentencia declare warning lanzará un aviso como el de la siguiente figura:

En un sistema más grande, podríamos queres escribir un consejo around para hacer que estas llamadas a métodos no fueran operacionales y testear los resultados antes de borrar realmente el código. Por ejemplo, en nuestro sistema, podríamos introducir el siguiente consejo para llamadas nulas al método AccessController.checkPermission() de la clase Account. Observa que el pointcut utilizado aquí es el mismo que el de la sentencia declare warning anterior. Una vez pasado el test, podrías eliminar el consejo:

El siguiente listado muestra el código de Account.java después de terminar el segundo paso:

Ahora ya hemos eliminado del código principal todos los chequeos de permisos y los hemos encapsulado en un aspecto separado. Podríamos parar aquí. Sin embargo, deberías considerar otros dos pasos opcionales. Aunque el primer paso opcional ayudará si evoluciona de cierta forma el código de la clase refactorizada, el segundo paso ayudará a extraer partes reutilizales del aspecto refactorizado.

. Paso 3 (Opcional): Simplificar la Definición del Punto de Corte.

En este paso opcional, redefinimos el pointcut para hacerlo más pequeño y semánticamente más significativo. Observa que en el paso 2, el pointcut definido capturaba sólo los métodos enumerados con una firma exacta a la suministrada. Esto nos ayuda a garantizar la preservación del comportamiento general, por ahora. Sin embargo, si se añaden posteriormente métodos que necesitan la misma funcionalidad, hacer esto requerirá la modificación de la definición del pointcut. Además, también nos gustaría protegernos contra los cambios en las firmas de los métodos que podrían evitar que se aplicara el consejo a los métodos modificados. En este paso, especificaremos una definición alternativa del pointcut para evitar este problema.

Observa que este paso no es un paso mecánico y requiere entendimiento de la interacción con la funcionalidad y la diligencia en la implementación. Como se podría llegar a un pointcut equivalente por muchos caminos, es importante que partamos de una expresión pointcut que semánticamente capture todos los puntos de unión requeridos. En la mayoría de las ocasiones, es una buena idea capturar un punto de corte que capture un amplio conjunto de puntos de unión basandose en la semántica y luego tener en cuenta los puntos de unión excepcionales. Por ejemplo, podríamos capturar todos los métodos públicos si les aplica la funcionalidad a todos los métodos disponibles externamente o a todos los métodos cuyo nombre empiece con "set" con cualquier modificador de estado. Si hay excepciones, podríamos mejorar el punto de corte con los casos excepcionales. De esta forma cualquier modificación en nuestro código (métodos añadidos o renombrados) será capturada correctamente.

En la clase Account, observamos que todos los métodos públicos excepto toString() llaman al método AccessController.checkPermission(). Capturemos está observación modificando la definición del pointcut. En este punto, podrías querer utilizar la vista crosscutting del IDE par ver si hemos capturados los métodos correctos. Observa que la inspección manual es tediosa y propensa a errores cuando se capturan puntos de corte en un gran número de puntos de unión. (Al final de esta página podrás ver una nota con una técnica alternativa para realizar esto).

El siguiente listado muestra la clase Account.java después de terminar el tercer paso:

Dado el tipo de cambios que hemos realizado, este es un momento especialmente bueno para ejecutar nuestros tests de prueba.

. Paso 4 (Opcional): Refactorizar el Aspecto de Refactorización

En este paso, podríamos refactorizar el propio aspecto de refactorización. Siguiendo el proceso ágil wisdom, probablemente querrás esperar hasta que veas que alguna otra clase necesita la misma refactorización. El proceso implica normalmente la creación de un aspecto abstracto y mover a él la mayoría de las funcionalidades. El aspecto base contiene unos cuantos puntos de corte y métodos abstractos. Los aspectos de refactorización concreta de cada módulo específico extienden este aspecto y proporcionan definición para los puntos de corte e implementación para los métodos. En un sentido, dicha refactorización es el equivalente AOP de la refactorización "Extract superclass", llamado "Extract base aspect".

. Utilizar Crosscutting Estáticos para Marcar Puntos de Unión no Capturados

Aunque en clases muy simples (como la clase Account), podrías crear un punto de corte directamente con una definición corta, para casos más complejos, hay una técnica mejor que ayuda a evitar los errores entre la definición del punto de corte original y su nueva versión. En esta técnica, creamos un punto de corte temporal que captura los puntos de unión según su semántica. En nuestro ejemplo de la clase Account, "parece" que todos los métodos públicos necesitan chequeo de permiso. Por lo tanto, definiremos el punto de corte para capturar todos los métodos públicos de la clase Account. Este punto de corte reemplazará la definición del punto de corte permissionCheckedExecution().

Ahora utilizaremos un idioma que produce errores si dos puntos de corte no coinciden exactamente en el mismo conjunto de puntos de unión. Observa que este idioma es sólo posible si ambos puntos de corte son estáticamente determinables, lo que significan que no deben utilizar puntos de corte this(), target(), args(), cflow(), cflowbelow(), ni if(). Frecuentemente, podemos refactorizar un mismo punto de corte para separar las partes estáticamente derterminables del resto, permitiendo el uso del idioma presentado. La sentencia que implementa este idioma para nuestro ejemplo, es esta:

Cualquier error lanzado por el compilador implica que dos puntos de corte no son equivalente. De la forma que los hemos configurado, obtenemos un error porque temp() captura el método toString() pero no a permissionCheckedExecution().

Como aparece en la figura anterior, el error apunta al método toString(). Reparemos esto elimando este método de los puntos de unión capturados. Ahora el compilador no producirá ningún error:

Observa que alguno de estos errores podrían revelar bugs en el código original, como llamadas a métodos saltadas desde otros métodos. Unos tests de pruebas inadecuados podrían haber dejado esos bugs en forma latente. En dichos casos, podría utilizar esta oportunidad para corregirlos y actualizar los tests.

Ahora que tenemos un punto de corte equivalente, cambiamos su nombre por el del punto de corte original y eliminamos éste.

Esta es la misma definición de punto de corte que creamos utilizando mera observación, como se describió en el paso 3.

COMPARTE ESTE ARTÍCULO

ENVIAR A UN AMIGO
COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN GOOGLE +
SIGUIENTE ARTÍCULO

¡SÉ EL PRIMERO EN COMENTAR!
Conéctate o Regístrate para dejar tu comentario.