Las Técnicas del Oficio
Introducción
En esta página, examinaremos técnicas AO para refactorizar el manejo de excepciones, el control de concurrencia, el goteo de argumentos, la creación de objetos worker, la implementación de interfaces, la sobreescritura de métodos, y el refuerzo de contratos. Por motivos de brevedad nos enfocaremos en el antes-y-después del código afectado por la refactorización y no mucho en los pasos que la conducen. El proceso de cada técnica es similar a los presentados en la página anterior. Además, asumimos que se han aplicado todas las técnicas de refactorización convencionales antes de empezar con la Refactorización AO. Esto evita la repetición de información cubierta en otros lugares, y muestra que la Refactorización AO de hecho mejora, y no reemplaza, a la refactorización convencional.
Muchas de las técnicas de Refactorización AO presentadas en esta página son útiles en implementaciones de crosscutting concerns en un gran ámbito. Sin embargo, la diferencía está en el énfasis aplicado a ciertos principios subrayados en la sección "Peculiaridades de la Refactorización AO" de la página anterior. En la práctica, es muy común empezar escribiendo un aspecto para refactorizar una clase, luego utilizar el aspecto para refactorizar varias clases, y eventualmente modificarlo para implementar el crosscutting de un modo general en el sistema.
Hay muchas técnicas que cubrir, empecemos ya!
Extraer el Manejo de Excepciones
El manejo de excepciones es un crosscutting concern que afecta a la mayoría de las clases no triviales. Debido a la estructura del código de manejo de excepciones (bloques try/catch), la refactorización convencional no puede realizar la extracción del código común. Toda clase (o conjunto de clases) podría tener su propia forma de manejar excepciones encontradas durante la ejecución de su lógica. Con la Refactorización AO, puedes extrear el código de manejo de excepciones a un aspecto separado.
Ilustraremos la técnica a través de un esquema de manejo de excepciones, donde los manejadores lanzan una nueva excepción convirtiendo la excepción capturada en otro tipo. Es posible ampliar este ejemplo a otras técnicas de manejo de excepciones como el "logging" o el "re-lanzado", el "abortado de transaciones", y el "re-intento de la operación".
Consideremos el patrón J2EE Business Delegate. Casi todos los métodos de un clase business delegate capturan excepciones lanzadas por la implementación subyacente y relanzan una excepción específica de la aplicación. Implementar este esquema de manejo de excepciones requiere un bloque try/catch en todos los métodos. En todos los bloques catch, creamos y lanzamos una nueva excepción que envuelve la excepción capturada, después realizamos otras tareas como el logging y la vuelta atrás de la transación actual. La misma situación ocurre en muchos otros patrones de diseño como Data Access Object y Service Locator.
Consideremos el siguiente listado, donde la clase LibraryDelegate utiliza el patrón Business Delegate. Hay lógica duplicada en casi todos los métodos:
Listado 1: LibraryDelegate.java antes de la refactorización:
No hay ninguna técnica de refactorización convencional que pueda extrear los bloques try/catch repetidos y el código asociado con cada uno. Para refactorizar toda la lógica del manejo de excepciones, escribriremos el siguiente aspecto. (Debido a un bug en la implementación actual de AspectJ 1.1.1, no podemos crear este aspecto como un aspecto anidado. Por lo tanto, haremos lo siguiente mejor -- crear un aspecto emparejado):

En el aspceto, todas las sentencias declare soft de LibaryExceptionHandlingAspect causan que el lanzamiento de una exceptión de los tipos especificados (RemoteException, ServiceLocatorException, CreateException) durante una llamada al punto de corte especificado sea tratado como una excepción de tiempo de ejecución. Cuando se lanza dicha excepción, se envuelve en una SoftException, que es una excepción en tiempo de ejecución. El consejo after throwing captura cualquier SoftException lanzada y lanza una nueva LibraryException envolviendo la excepción original obtenida llamando a getWrappedThrowable() sobre la excepción capturada. Ahora podemos extraer todo el manejo de excepciones de la implementación principal. El Listado 2 muestra la clase LibraryDelegate.java después de aplicar la refactorización “Extract exception handling”. Observa que aunque los puntos de corte del aspecto anterior utilizan comodines, podrías llegar a la misma definición siguiendo el proceso descrito en la página anterior.
Listado 2: LibraryDelegate.java después de la refactorización:
Claramente hay un ahorro substancial de código y el código principal está más claro. El aspecto ha localizado el manejo de excepciones, simplificando así la modificación de la política de manejo de excepciones. Por ejemplo, es fácil añadir logging modificando el consejo.
Una técnica de refactorización normalmente lleva a otra. La refactorización descrita a continuación normalmente empieza con una combinación de “extract method calls” y “extract exception handing”.
Extraer el Control de Concurrencia
El control de concurrencia normalmente es un crosscutting concern de importancia crítica, pero igualmente difícil de implementar. Además de ser difícil de entender, una implementación de control de concurrencia requiere que el código esté esparcido por muchos métodos. Hay disponibles unos cuantos patrones para el control de concurrencia, y aunque los conceptos son reutilizables, sus implememtaciones no. AOP ofrece implementaciones reutilizables de estos patrones, aliviando un poco el dolor de la implementación del control de concurrencia.
En el Listado 3 hay un ejemplo sencillo que utiliza el patrón read-write lock. El patrón de concurrencia requiere el manejo de dos tipos de bloqueo: lectura y escritura. Antes de entrar en un método que sólo lee datos, se adquiere el bloqueo de lectura y este bloqueo se libera antes de abandonar el método. El bloqueo de escritura se adquire y libera de forma similar en todos los métodos que modifican los datos.
Listado 3: Account.java antes de la refactorización:
La solución utiliza un aspecto ReadWriteLockSynchronizationAspect reutilizable (puedes ver su
código fuente). El aspecto declara dos puntos de corte abstractos: readOperations() y writeOperations(), y les aconseja manejar los bloqueos de lectura y escritura. El aspecto utiliza una asociación perthis para maximizar la reutilización del código. Sin embargo, los detales de esta asociación van más allá del ámbito de este tutorial. Observa que la base del aspecto reutilizable podría ser un resultado de seguir el paso "Refactorizar el Aspecto de Refactorización" que hemos visto en la página anterior.
Hemos refactorizado el códido del control de concurrencia de la clase Account en un aspecto anidado. Este aspecto es un sub-aspecto concreto de ReadWriteLockSynchronizationAspect. Proporcionamos las definiciones para los puntos de corte asbtractos: readOperations() y writeOperations(). En el caso de la clase Account (depsués de seguir el proceso descrito en la página anterior) hemos definido operaciones de lectura en todos los métodos cuyo nombre empieza con get además del método toString(). Consideramos cualquier otro método como una operación de escritura:
El siguiente listado muestra la clase Account.java después de aplicar la refactorización para encapsular el control de concurrencia en el aspecto anidado.
Listado 3: Account.java después de la refactorización:
La refactorización ahorra gran cantidad de código, asegura una adquisión y liberación consistente de los derechos de bloqueo, y aisla los detalles del patrón de bloqueo lectura-escritura. Como algo extra, podremos cambiar fácilmente la implementación. Por ejemplo podemos utilizar un esquema más simple que rodea cada operación de lectura o escritura en un bloque synchronized. Todo lo que necesitamos es modificar el aspecto base:
El aspecto base reutilizable SimpleSynchronizationAspect (puedes ver su
código fuente) simplemente rodea cada punto de unión capturado con un bloque synchronized. El uso de AOP creó la separación de conceptos entre la implementación de Account y la implementación del control de concurrencia, lo que nos permitió realizar fácilmente sta modificación.
Las técnicas descritas hasta ahora utilizan las construcciones de crosscutting concerts más simples. El uso de patrones de diseño AOP ayuda a la creación de técnicas de refactorización poderosas. Las dos técnicas hacen uso de patrones de diseño AOP para extraer el código común que se repite en muchos métodos.
Extraer la Creación de Objetos Worker
Un objeto worker es un ejemplar de una clase que encapsula un método (llamado método worker) para que ese método pueda ser tratado como un objeto. Necesitamos crear objetos worker en muchas situaciones: ejecución asíncrona de métodos, autorización utilizando el API Java Authentication and Authorization Service (JAAS), implementación de threads seguros en aplicaciones Swing/SWT, etc. En todas las ocasiones, se necesita mucho código extra para crear dichos objetos worker. Por cada método worker necesitamos crear una clase con nombre o anónima que encapsule la llamada al método requerido y crear un ejemplar de dicha clase. Si utilizamos clases con nombre, terminaremos con muchas clases que sólo ejecutan un método worker y si utilizamos clases anónimas, el código para crear la clase emborrona la lógica principal. El resultado es un código difícil de entender y de mantener.
Podemos idear una Refactorización AO que utilice el patrón de creación de objetos worker para ayudarnos a dejar los más sencilla posible la implementación principal y para localizar la creación del worker y la lógica utilizada.
El siguiente listado muestra la clase ATM antes de la refactorización. El uso del esquema de autorización JAAS requiere la ejecución del método pasando un objeto worker a Subject.doAsPrivileged(). Por lo tanto, utilizamos una clase anónima que ejecute la lógica de negocio en su método run(). Pasamos un ejemplar de la clase anónima a Subject.doAsPrivileged() (en la implementación general, deberíamos llamar a checkPermission() en los métodos que necesiten el chequo de autorización).
Listado 5: ATM.java antes de la refactorización:
Claramente, el código es difícil de entender y cuesta mucho imaginarse la lógica de negocio enmarañada entre esa cantidad de código de clases anónimas. Podemos refactorizar la lógica de autorización utilizando el patrón de creación de objetos worker mostrado en el siguiente código:
Listado 6: ATM.java después de la refactorización:
Observa que si buscamos los métodos que lanzan una excepción específica del negocio, necesitaremos lógica adicional en el aspecto para tratarlos. Por ejemplo, en el aspecto anterior, cuando lo aplicamos al método debit() que lanza InsufficientBalanceException, el cliente recibe una BankingException genérica. Sin embargo, no consideremos este problema en esté tutorial.
Hasta ahora, nos hemos enfocado en la refactorización de funcionalidades comunes en una clase a la vez. Las dos siguientes técnicas de refactorización consideran un conjunto de clases relacionadas como el objetivo de la refactorización.
Reemplazar el Goteo de Argumentos por Agujeros de Gusano
Con frecuencia, hay necesidad de pasar una parte del contexto actual (ejecución actual/objeto objetivo, argumentos de métodos, etc.) a los métodos invocados, que a su vez se siguen pasando, para que un método más abajo en la cadena de llamadas eventualmente pueda utilizar el contexto para realizar su tarea. El resultado es un API contaminado debido a un crosscutting concern que incrementa el embrronado de la clase del medio del cadena. Con la Refactorización AO, podemos evitar pasar parámetros a los métodos de la cadena de llamadas. Esta es una de las técnicas de Refactorización AO más invasivas ya que normalmente se aplica a dos o más clases.
Consideremos los fragmentos de unas clases (Listado 7) de un sistema bancario. La clase ATM utiliza un ejemplar de BankingLiaison (que representa al banco correspondiente a la tarjeta ATM) que, a su vez, llama a operaciones sobre el ejemplar Account. La generación de sentencias Account necesita información sobre la ATM además de la cuenta, la cantidad, y la operación implicada. Para facilitar la generación de sentencias, los métodos de ATM pasan el propio ATM a los métodos de BankLiaison, que los propagan a los métodos de la clase Account. Los métodos de la case Account llaman a appendAccountActivities() que añade las actividades, a una tabla de la base de datos.
Listado 7: clases del sistema bancario antes de la refactorización:
El problema con el código anterior es que el parámetro ATM gotea a través de capas de llamadas. Observa que el ejemplo sólo utiliza tres niveles de clases y un parámetro. El problema se acucia según se incrementan el número de clases y de parámetros.
Idearemos una técnica de Refactorización AO basada en el patrón Wormhole (agujero de gusano) para ayudarnos en esta situación. Este patrón utiliza dos puntos de corte - uno para el llamador que tiene el contexto y otro para el llamado que necesita el contexto. Luego el patrón crea un wormhole utilizando un punto de corte cflow() y transfiere el contexto llamador al punto de lamada. Observa que hay una alternativa utilizando una variable ThreadLocal, para contener la variable ATM, pero la solución que utiliza esta patrón es más clara porque modulariza el acceso a la información, en vez de utilizar una variable global. Como efecto lateral, incluso el esquema de utilización de una variable ThreadLocal es más efectivo cuando se utiliza con aspectos porque ya está modularizado!
Refactorizamos la lógica de actualización de las tabla de actividades de account en un aspecto anidado en la clase Account. Aunque mostremos directamente el resultado final de la refactorización, aplicar “Extract method calls” antes de utilizar el patrón wormhole podría ser una buena idea. Aquí está el aspecto que realiza la refactorización:
La razón por la que hemos elegido implementar el aspecto como un aspecto anidado de la clase Account y no en la clase ATM, es evitar una dependencia adicional comparada a la implementación convencional. Un aspecto separado de nivel superior también hubiera sido una buena idea. Observa que hemos movido el método appendAccountActivities() desde la clase Account al aspecto, ya que sólo lo utiliza él.
Listado 8: Las clases del sistema bancario después de la refactorización:
Las clases ATM, BankingLiaison, y Account ya no necesitan el parámetro ATM adicional que se utiliza con el único propósito de actualizar la tabla de actividades de cuentas de la base de datos.
Hasta ahora, hemos tratado con técnicas de crosscutting dinámicos para Refactorización AO. La siguiente técnica ilustra el uso de crosscutting estático ofrecido por AspectJ para refactorizar código ya existente.
Extraer las Implementaciones de Interfaces
La técnica de refactorización convencional de “Extract interface” permite mejorar el desacoplamiento de los clientes y las implementaciones. Si más de una clase implementa ese interface, podría terminar duplicando el código requerido para implementar el interface. Con AOP, se puede llevar a cabo la idea y evitar cualquier duplicación.
La Refactorización AO utiliza el mecanismo de declaración inter-tipos de AspectJ (también conocido como presentación). Podemos escribir un aspecto que presente la implementación por defecto en el interface extraído. En esencia, AspectJ permite implementar mezclas. Este tipo de refactorización es especialmente útil cuando la implementación de un interface es el punto de ebullición.
Consideremos el interface ServiceCenter del siguiente listado:
Listado 9: ServiceCenter.java antes de la refactorización:
La clase ATM implementa el interface ServiceCenter.
Listado 10: ATM.java antes de la refactorización:
La clase ATM contiene una implementación primordial de ServiceCenter. Otras clases como BrickAndMortarBank, SuperStoreServiceCenter serían similares - todas repiten el código de la implementación del interface ServiceCenter. Aunque podríamos evitar el código duplicado creando la implementación por defecto del interface y hacer que las clases desciendan de esta implementación, la técnica no funcionará para las clases que descienden de otras clases.
Con la Refactorización AO, presentamos la implementación por defecto en el interface ServiceCenter utilizando un aspecto anidado que podemos ver en el siguiente listado:
Listado 11: ServiceCenter.java después de la refactorización:
En el listado anterior, el aspecto anidado IMPL presenta los datos miembros así como los métodos de la implementación por defecto del interface ServiceCenter. Con este aspecto, cualqueir clase que implemente el interface automáticamente hereda la implementación por defecto.
Ahor apodemos tomar la implementación de los métodos declarados en ServiceCenter en la clase ATM, como se ve en el siguiente listado:
Listado 12: ATM.java después de la refactorización:
Las otras clases que implementan el interface, como BrickAndMortarBank, SuperStoreBranch también pueden eliminar la implementación de los métodos declarados en el interface. La implementación de las clases ya no incluye el código importante para implementar los interfaces. Pero aún así, estas clases pueden sobreescribir los métodos proporcionados por la implementación por defecto del aspecto.
Hay algunas variaciones para esta técnica de refactorización. Primero podríamos permitir que las implementaciones de las clases eligieran si heredar o no la implementación por defecto proporcionada por el aspecto. Una forma de implementar este esquema es crear un subinterface del interface original, digamos ServiceCenterDefaultAspectImpl, y dejar que el aspecto presente la implementación por defecto de este interface. Las clases heredarán la implementación por defecto sólo si declaran que implementan ServiceCenterDefaultAspectImpl. Otra variación es proporcionar la implementación por defecto como parte del interface. Aunque en algunos casos, dicha elección podría pasar a ser una necesidad (debido a una suerte de información necesaria para la implementación), en otros casos, esto podría pensarse como una decisión de diseño para forzar a los desarrolladores de la clase a pensar en la semántica de la implementación correcta.
El Resto en Breve
En esta página, hemos examinado algunas técnicas de Refactorización AO. Concluiremos con una breve explicación de otras técnicas.
Reemplazar la Sobreescritura con un Consejo
Es frecuente que necesitemos una mejora adicional en el comportamiento común de muchos métodos de una clase. Una solución típica es crear una subclase y sobreescribir los métodos para realizar alguna lógica adicional. Por ejemplo, podríamos tener una clase modelo sin ningún tipo de soporte para notificación de observadores. Puedes añadir dicho soporte creando una subclase y sobrescribiendo todos los métodos modificadores del estado para notificarselo a los observadores.
Con Refactorización AO, puedes utilizar un aspecto para aconsejar al método necesario con la lógica adicional. Por ejemplo, en lugar de sobreescribir, simplemente podrías aconsejar los métodos de la subclase que notifiquen a los observadores. La implementación es muy parecida a la técnica “Extract method calls”. La diferencia es que una vez extraidas las llamadas a métodos, los métodos de la subclase simplemente llaman al método de la correspondiente clase base, y por lo tanto no necesita existir en la clase derivada.
Extraer la Inicialización Lenta
La inicialización lenta de recursos costosos es una técnica de optimización común. La solución convencional requiere chequeos de los recursos no inicilizados en cada lugar en que se usen esos recursos y provoca el código-borroso y el código-mezclado. Podría parecer que se pueden solucionar estos problemas simplemente accediendo a la variable de ejemplar mediante su métodos get; sin embargo, hay una pega: si una porción de la clase contenedora de la referencia al recurso accede a él directamente, la inicialización no ocurrirá y experimentaremos consecuencias indeseadas. Los tests podría revelar bugs como éste, pero sólo si se llama al método erróneo antes de cualquier otro método que si haga la inicialización del recurso. Observa que seleccionando el accceso como private a los miembros de la clase evitará el acceso directo desde otras clases, pero no desde dentro de la propia clase. Con la Refactorización AO, podemos aconsejar a los accesos de lectura del recurso (utilizando un punto de corte get()) para que inicialicen.
Extraer el Refuerzo de Contratos
El refuerzo de contratos normalmente requiere la duplicación de código en muchos métodos de una clase. Esto es especialmente cierto cuando se implementan clases que no varían y condiciones pre y post que son comunes a muchos métodos. Las implementaciones convencionales requieren añadir código idéntico - chequeo condicional y sentencias de asserto - en muchos métodos. Con la Refactorización AO, podemos refactorizar dichos chequeos de contratos en un aspecto separado. Este tipo de refactorización es muy parecida a “Extract method calls”, excepto en que normalmente utiliza sentencias de assertos para realizar la lógica adicional.
Conclusión
Los aspectos que nacen de la refactorización normalmente se construyen afectando a una pequeña porción del sistema, empezando normalmente con una clase. Aunque la límitación del ámbito reduce los beneficios de esos aspectos, también reduce el riesgo de cambios no deseados en el comportamiento del sistema. Con el tiempo, las técnicas de refactorización se pueden construir sobre ámbitos más amplios. En este tutorial hemos examinado varias técnicas con ejemplos típicos que se puede encontrar un desarrollador Java. Inicialmente, podrías utilizar las técnicas en los escenarios descritos. Con el tiempo, desarrollará un buen ojo para aplicar esas técnicas a diferentes escenarios e incluso podrías descubrir nuevas técnicas.
Las técnicas de refactorización son útiles para entenderlas por sí mismas. Sin embargo, será mucho mejor cuando los IDEs ayuden en la Refactorización AO como ya lo hacen con la refactorización convencional. Un simple soporte de Refactorización AO permitiría a los programadores elegir entre múltiples bloques de código y el tipo de refactorización deseada. El IDE podría crear una versión sencilla de un apsecto que encapsulara los elementos comunes del código seleccionado. Si fuera apropiado, los desarrolladores podría mejorar las definiciones de los puntos de corte del aspecto creado por el IDE.
La AOP beneficia significativamente a la programación del mundo real. Sin embargo, de forma poco entendible, hay muchas precauciones para adoptarla. Un camino de adaptación seguro para cualquier tecnología es aquel que permite un uso gradual. Para AOP, dicho camino parece ser la utilización de aspectos desarrollados inicialmente, seguido por la refactorización utilizando técnicas de AO, luego implementando los crosscutting concerns complejos, y finalmente diseñando sistemas con AOP desde la concepción del proyecto. Dicho camino minimiza los riesgos asociados, mejora el entendimiento de los fundamentos de AOP, crea confianza, da una realización a los usos apropiados o inapropiados, recurriendo a los patrones de diseño, y todo ello disparando la confianza en la AOP.
Los beneficios de la AOP son demasiado reales para ignorarlos, y una aproximación cautelosa es demasiado válida como para perdérsela. La utilización del camino seguro para incorporar los aspecto del desarrollo y la Refactorización AO ayudan a sobrellevar este dilema. Primero, acomodate con el uso de los aspectos de desarrollo en AOP. Luego cuando veas código duplicado que no puedas refactorizar utilizando las técnicas convencionales (no tendrás que mirar mucho!), aprovecha la oportunidad para utilizar la Refactorización AO. Obtendrás beneficios inmediatos y habrás dado los pasos para alcanzar el poder de la Programación Orienta al Aspecto.