Tecnología de vanguardia

Programación orientada a aspectos, intercepción y Unity 2.0

Dino Esposito

Dino EspositoNo cabe duda de que la orientación a objetos es un paradigma convencional de programación, uno excelente para el desglose de un sistema hasta sus componentes y para describir procesos por medio de componentes. El paradigma de orientación a objetos (OO) también es excelente para lidiar con asuntos específicos de un componente. Sin embargo, el paradigma de OO no es tan efectivo para tratar con asuntos de transversalidad. En general, un asunto de transversalidad es algo que afecta varios componentes en un sistema.

Para maximizar la reutilización de código de lógica empresarial compleja, generalmente se tiende a diseñar una jerarquía de clases alrededor de las funciones principales y las funciones empresariales primarias del sistema. Pero, ¿qué pasa con otros asuntos no específicos para empresas que son transversales a la jerarquía de clases? ¿Dónde figuran características tales como el almacenamiento en memoria caché, la seguridad y el registro? Muy probablemente, terminen repitiéndose en cada objeto afectado.

Un asunto de transversalidad es un aspecto del sistema que, sin ser una responsabilidad específica de un componente o familia de componentes en especial, debe tratarse en un nivel lógico distinto, un nivel más allá de las clases de aplicación. Por esta razón se definió un paradigma de programación distinto años atrás: la programación orientada a aspectos (AOP). Por cierto, el concepto de AOP se desarrolló en los laboratorios de Xerox PARC en la década de 1990. El equipo también desarrolló el primer (y actualmente el más popular) lenguaje de AOP: AspectJ.

Aunque casi todos concuerdan en los beneficios de AOP, actualmente su implementación no es muy extendida. En mi opinión, la razón más importante para su adopción tan limitada es, esencialmente, la falta de herramientas adecuadas. Estoy bastante seguro de que el día en el que AOP sea compatible de forma nativa (incluso si esto fuera parcialmente) con .NET Framework de Microsoft, esto será un hito en la historia de AOP. Hoy en día, sólo se puede usar AOP en .NET usando marcos ad hoc.

La herramienta más poderosa para AOP en .NET es PostSharp, disponible en sharpcrafters.com. PostSharp ofrece un marco de AOP completo, donde puede experimentar todas las características claves de la teoría de AOP. Sin embargo, debe tomar en cuenta que muchos marcos de inserción de dependencia (DI) incluyen algunas capacidades de AOP.

Por ejemplo, encontrará capacidades de AOP en Spring.NET, Castle Windsor y, por supuesto, Microsoft Unity. Usualmente, para escenarios relativamente simples, tales como trazado, almacenamiento en memoria caché y decoración de componentes en el nivel de aplicación, las capacidades de los marcos de DI bastan. Sin embargo, es difícil usar marcos DI en lo referente a objetos de dominio y objetos de UI. Un asunto de transversalidad ciertamente puede verse como una dependencia externa y las técnicas de DI ciertamente permiten insertar dependencias externas a una clase.

El punto es que DI probablemente necesitará diseño ad hoc por adelantado o un poco de refactorización. En otras palabras, si ya está usando un marco de DI, entonces es fácil trabajar con algunas características de AOP. En cambio, si su sistema no utiliza DI, trabajar en un marco de DI puede exigir bastante trabajo. Puede que esto no sea siempre posible en un proyecto grande o durante la actualización de un sistema heredado. En lugar de esto, con un acercamiento clásico a AOP, podrá abarcar cualquier asunto de transversalidad en un componente nuevo llamado aspecto. En este artículo, primero daré información general rápida del paradigma orientado a aspectos y después pasaré a ver las capacidades relacionadas con AOP que encontrará en Unity 2.0.

Una guía rápida para AOP

Un proyecto de programación orientada a objetos (OOP) se crea a partir de varios archivos fuente, cada uno de los cuales implementa una o más clases. El proyecto también incluye clases que representan asuntos de transversalidad, como registro o almacenamiento de memoria caché. Un compilador procesa todas las clases y produce código ejecutable. En AOP, un aspecto es un componente reutilizable que encapsula el comportamiento necesario para varias clases en el proyecto. La manera en la que efectivamente se procesan los aspectos depende de la tecnología AOP que esté considerando. En general, se puede decir que el compilador no procesa de forma simple y directa los aspectos. Es necesaria una herramienta de tecnología específica adicional para modificar el código ejecutable para que este tome aspectos en cuenta. Consideremos por un minuto lo que pasa con AspectJ, un compilador AOP de Java que fue la primera herramienta AOP creada.

Con AspectJ, puede usar el lenguaje de programación Java para escribir sus clases y el lenguaje AspectJ para escribir aspectos. AspectJ es compatible con una sintaxis personalizada, a través de la cual se indica el comportamiento esperado del aspecto. Por ejemplo, un aspecto de registro puede especificar que se hará un registro antes y después que se invoque cierto método. De alguna manera, los aspectos se fusionan con el código de fuente regular y se produce una versión intermedia del código fuente que se compilará en un formato ejecutable. En la jerga de AspectJ, el componente que preprocesa aspectos y los combina con el código fuente es conocido como el tejedor. Éste produce una salida que el compilador puede presentar en un ejecutable.

En síntesis, un aspecto describe un fragmento reutilizable de código que se quiere insertar en clases existentes, sin tocar el código fuente de dichas clases. En otros marcos de AOP (como el marco .NET PostSharp), no encontrará el tejedor. Sin embargo, el marco siempre procesa el contenido de un aspecto, lo que resulta en alguna forma de inserción de código.

Observe que, en este aspecto, la inserción de código es diferente de la inserción de dependencia. Con inserción de código nos referimos a la capacidad de un marco AOP de insertar llamadas a extremos públicos del aspecto en puntos específicos en el cuerpo de clases decorado con cierto aspecto. El marco PostSharp, por ejemplo, permite escribir aspectos como atributos .NET que se adjuntan a los métodos en las clases. Los atributos de PostSharp se procesan con el compilador de PostSharp (podríamos llamar a esto el tejedor) en un paso posterior a la compilación. El efecto final es que el código se mejora, incluyendo parte del código en los atributos. Pero los puntos de inserción se resuelven de forma automática, de manera que todo lo que debe hacer en su papel de desarrollador es escribir un componente independiente de aspecto y adjuntarlo a un método público de clase. El código es fácil de escribir y aún más fácil de mantener.

Para finalizar esta breve información general sobre AOP, permítanme presentar algunos términos específicos y aclarar el significado que tienen. Un join point (punto de unión) indica un punto en el código fuente de la clase destino donde desea insertar el código del aspecto. Un pointcut (punto de corte) representa una colección de puntos de unión. Un advise (consejo) hace referencia al código a insertar en la clase destino. El código se puede insertar antes, durante y después del punto de unión. Un consejo se asocia con un punto de corte. Estos términos provienen de la definición original de AOP y puede que no se reflejen de forma literal en el marco de AOP específico que esté usando. Es recomendable intentar captar el concepto que representan estos términos, los cuales son los pilares de AOP, y después usar este conocimiento para comprender mejor los detalles de un marco específico.

Una guía rápida para Unity 2.0

Unity es un bloque de aplicación disponible como parte del proyecto Microsoft Enterprise Library, además de estar disponible como una descarga por separado. Microsoft Enterprise Library es una colección de bloques de aplicación que abordan varios asuntos de transversalidad que caracterizan el desarrollo de aplicaciones .NET: registro, almacenamiento en memoria caché, criptografía, control de excepciones y más. La última versión de Enterprise Library es la 5.0, lanzada en abril de 2010. Ésta es totalmente compatible con Visual Studio 2010 (obtenga más información acerca de esto en el centro para desarrolladores patrones y prácticas en msdn.microsoft.com/library/ff632023).

Unity es uno de los bloques de aplicación de Enterprise Library. Unity, que también está disponible para Silverlight, es esencialmente un contenedor DI con compatibilidad adicional para un mecanismo de intercepción, por medio del cual las clases se pueden orientar más a aspectos.

Intercepción en Unity 2.0

La idea central de la intercepción en Unity es permitir a los desarrolladores personalizar la cadena de llamadas que toma invocar a un método o a un objeto. Es decir, el mecanismo de intercepción de Unity captura las llamadas que se están haciendo a objetos configurados y personaliza el comportamiento de los objetos destino al agregar código adicional antes, después o durante de la ejecución regular de métodos. La intercepción es, en esencia, un enfoque extremadamente flexible hacia la adición de nuevos comportamientos a un objeto durante la ejecución, sin tocar el código fuente y sin afectar el comportamiento de clases en la misma ruta de herencia. La intercepción de Unity es una manera de implementar el patrón Decorator, el cual es un patrón popular diseñado para extender la funcionalidad de un objeto durante la ejecución, mientras se usa el objeto. Un decorator es un objeto contenedor que recibe (y mantiene una referencia a) una instancia del objeto destino y aumenta sus capacidades hacia el mundo exterior.

El mecanismo de intercepción de Unity 2.0 es compatible con la intercepción tanto de instancia como de tipo. Lo que es más, la intercepción funciona independientemente de la manera en la que se realiza la instancia del objeto, independientemente de que el objeto se cree por medio del contenedor de Unity o si es una instancia conocida. En el segundo caso, puede usar simplemente una API diferente y completamente independiente. Sin embargo, si hace eso, perderá la compatibilidad con configuración de archivos. La figura 1 muestra la arquitectura de la característica de intercepción de Unity y detalla cómo funciona en una instancia de objeto específica no resuelta por medio del contenedor (la figura es sólo una versión levemente retocada de una imagen que encontrará en la documentación de MSDN).

image: Object Interception at Work in Unity 2.0

Figura 1 Intercepción de objetos en funcionamiento en Unity 2.0

El subsistema de intercepción se compone de tres elementos clave: el interceptor (o proxy), la canalización de comportamiento y el comportamiento o aspecto. En ambos extremos de los subsistemas, encontrará la aplicación de cliente y el objeto destino; es decir, el objeto al que se le asignan comportamientos adicionales no codificados en el código fuente. Una vez que la aplicación del cliente se configura para usar la API de intercepción de Unity en una instancia dada, cualquier método de invocación pasa por medio de un objeto proxy: el interceptor. Este objeto proxy busca en la lista de comportamientos registrados y los invoca por medio de la canalización interna. Se permite que se ejecute cada comportamiento configurado antes o después de la invocación regular del método de objeto. El proxy inserta datos de entrada a la canalización y recibe cualquier valor devuelto como se generó inicialmente por el objeto destino, el cual se modificó posteriormente por comportamientos.

Configuración de intercepción

La manera recomendada de usar intercepción en Unity 2.0 es distinta a la de las versiones anteriores, aunque el enfoque usado en versiones anteriores es totalmente compatible con la versión actual. En Unity 2.0, la intercepción es sólo una nueva extensión a agregar al contenedor para describir la manera en la que se resuelve un objeto. Este es el código que necesita si desea configurar la intercepción por medio de código de fluent:

var container = new UnityContainer();
container.AddNewExtension<Interception>();

El contenedor necesita encontrar la información acerca de los tipos a interceptar y los comportamientos a agregar. Esta información se puede agregar ya sea usando código fluent o por medio de configuración. Considero que la configuración es particularmente flexible, puesto que permite modificar cosas sin tocar la aplicación y sin ningún paso nuevo de compilación. Optemos por el enfoque basado en configuración.

Para empezar, agregue lo siguiente en el archivo de configuración:

<sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.
  Configuration.InterceptionConfigurationExtension, 
  Microsoft.Practices.Unity.Interception.Configuration"/>

La finalidad de este script es extender el esquema de configuración con nuevos elementos y alias específicos para el subsistema de intercepción. Otra adición necesaria es la siguiente:

<container> 
  <extension type="Interception" /> 
  <register type="IBankAccount" mapTo="BankAccount"> 
    <interceptor type="InterfaceInterceptor" /> 
    <interceptionBehavior type="TraceBehavior" /> 
  </register> 
</container>

Para obtener lo mismo usando código fluent, debe llamar a AddNewExtension<T> y RegisterType<T> en el objeto contenedor.

Observemos más detenidamente al script de configuración. El elemento <extension> agrega intercepción al contenedor. Observe que la intercepción que se usa en el script es uno de los alias definidos en la extensión de la sección. El tipo de interfaz IBankAccount está asignado al tipo concreto BankAccount (esta es la función típica de un contenedor DI) y está asociado con un tipo particular de interceptor. Unity ofrece dos tipos principales de interceptores: los interceptores de instancia y los interceptores de tipo. En el próximo mes, profundizaré sobre los interceptores. Por ahora, basta decir que un interceptor de instancia crea un proxy para filtrar llamadas entrantes dirigidas a la instancia interceptada. Los interceptores de tipo, por otra parte, simplemente imitan el tipo del objeto interceptado y trabajan en una instancia del tipo derivado (para más información acerca de los interceptores, consulte msdn.microsoft.com/library/ff660861(PandP.20)).

El interceptor de interfaz es un interceptor de instancia limitado a actuar como el proxy de sólo una interfaz del objeto. El interceptor de interfaz usa generación de código dinámica para crear la clase proxy. El elemento de intercepción de comportamiento en la configuración indica el código externo que desea ejecutar alrededor de la instancia de objeto interceptado. Se debe configurar declarativamente la clase TraceBehavior para que el contenedor pueda resolver a éste y a cualquiera de sus dependencias. Puede usar el elemento <register> para indicar al contenedor la clase TraceBehavior y su constructor esperado, como se muestra aquí:

<register type="TraceBehavior"> 
   <constructor> 
     <param name="source" dependencyName="interception" /> 
   </constructor> 
</register>

La Figura 2 muestra un resumen de la clase TraceBehavior.

Figura 2 Un comportamiento de muestra de Unity

class TraceBehavior : IInterceptionBehavior, IDisposable
{
  private TraceSource source;

  public TraceBehavior(TraceSource source)
  {
    if (source == null) 
      throw new ArgumentNullException("source");

    this.source = source;
  }
   
  public IEnumerable<Type> GetRequiredInterfaces()
  {
    return Type.EmptyTypes;
  }

  public IMethodReturn Invoke(IMethodInvocation input, 
    GetNextInterceptionBehaviorDelegate getNext)
  {
     // BEFORE the target method execution 
     this.source.TraceInformation("Invoking {0}",
       input.MethodBase.ToString());

     // Yield to the next module in the pipeline
     var methodReturn = getNext().Invoke(input, getNext);

     // AFTER the target method execution 
     if (methodReturn.Exception == null)
     {
       this.source.TraceInformation("Successfully finished {0}",
         input.MethodBase.ToString());
     }
     else
     {
       this.source.TraceInformation(
         "Finished {0} with exception {1}: {2}",
         input.MethodBase.ToString(),
         methodReturn.Exception.GetType().Name,
         methodReturn.Exception.Message);
     }

     this.source.Flush();
     return methodReturn;
   }

   public bool WillExecute
   {
     get { return true; }
   }

   public void Dispose()
   {
     this.source.Close();
   }
 }

Una clase behavior implementa IInterceptionBehavior, el cual consiste básicamente del método Invoke. El método Invoke contiene toda la lógica que desea usar para cualquier método bajo el control del interceptor. Si desea hacer algo antes que se llame al método objetivo, puede hacerlo al principio del método. Cuando desee proseguir con el objeto destino (o más precisamente, con el siguiente comportamiento registrado en la canalización), llame al delegado getNext proporcionado por el marco. Para finalizar, puede usar cualquier código que desee para procesar posteriormente el objeto destino. El método Invoke necesita devolver una referencia al siguiente elemento en la canalización. De devolverse un valor nulo, entonces la cadena se interrumpe y no se invoca a ningún comportamiento futuro.

Flexibilidad de configuración

La intercepción y, más generalmente, AOP, abordan varios escenarios interesantes. Por ejemplo, la intercepción permite agregar responsabilidades a los objetos individuos, sin modificar la clase entera, lo que conserva la solución mucho más flexible de lo que sería con decorator.

Este artículo fue sólo un esbozo de AOP aplicado a .NET. En los siguientes meses, escribiré más acerca de la intercepción en Unity y de AOP en general.

Dino Esposito es el autor del libro “Programming Microsoft ASP.NET MVC” de Microsoft Press (2010) y coautor de “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2008). Con residencia en Italia, Esposito participa habitualmente en conferencias y eventos del sector en todo el mundo. Puede participar en su blog en weblogs.asp.net/despos.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Chris Tavares