AOP con Castle Windsor: IInterceptor

La Programación Orientada a Aspectos (AOP) es un paradigma de programación que trata de incrementar la modularidad de las aplicaciones aislando aquellos aspectos que afectan a muchas partes de la aplicación sin ser resposabilidad expresa de ninguna de ellas (cross-cutting concerns). Normalmente se usa asociada a OOP y permite que la responsabilidad de las clases quede mucho más definida.

Un ejemplo sencillo sería generar un traza para instrumentar la aplicación y saber qué metodos se están invocando. Si hacemos eso manualmente tendríamos algo así:

public class Calculator : ICalculator
{
    public int Sum(int a, int b)
    {
        log.Write("Enter Sum()");
        var result = a + b;
        log.Write("Exit Sum()");
        return result;
    }
}

Esta implementación tiene dos problemas fundamentales:

  • Obliga a introducir en Calculator código que no tiene nada que ver con su responsabilidad real, lo que supone una violación del SRP.
  • Duplica el código para trazar la entrada y salida del método por toda la aplicación, por lo que no cumple con el principio DRY.

Con AOP podemos definir un aspecto y aplicárselo a los métodos que sea necesario para que se escriba la información en log. De esta forma evitamos contaminar todas las clases con la implementación del aspecto.

Seguramente el framework más utilizado y más completo en C# para implementar AOP sea PostSharp, pero si estás utilizando Castle Windsor como contenedor de inversión de control, puedes aprovecharlo para usar ciertas técnicas de AOP.

Crear un interceptor

Para utilizar AOP con Castle Windsor existe una herramienta muy potente: los interceptores.

Un interceptor es una clase que implementa el interface IInterceptor y que nos permite interceptar las llamadas a los métodos de una clase, pudiendo actuar antes o después de la invocación al método. Para hacer esto, Castle Windsor se apoya en otro proyecto de Castle, Dynamic Proxy, que permite generar «al vuelo» proxies usando generación ligera de código (LCG).

En realidad, un interceptor no es más que una forma de implementar un patrón clásico: el patrón decorador. La diferencia es que usando interceptores podemos generar dinámicamente los decoradores, sin necesidad de declararlos previamente.

Para crear un interceptor, sólo necesitamos implementar el interface IInterceptor. Sin embargo, Castle nos ofrece una clase base StandardInterceptor, que a través de un method template nos ofrece los puntos de enganche apropiados para facilitarnos las cosas.

Usando el ejemplo anterior de la traza, tendríamos un interceptor así:

public class TraceInterceptor : StandardInterceptor
{
   private readonly ITraceWriter writer;

   public TraceInterceptor(ITraceWriter writer)
   {
      this.writer = writer;
   }

   protected override void PreProceed(IInvocation invocation)
   {
       writer.Write("Enter: {0}", invocation.Method.Name);
   }

   protected override void PostProceed(IInvocation invocation)
   {
       writer.Write("Exit: {0}", invocation.Method.Name);
   }
}

El objeto IInvocation contiene toda la información del método que estamos interceptando, incluyendo el propio método, los valores de los argumentos que está recibiendo y, tras la invocación, el valor de retorno. En este ejemplo únicamente estamos añadiendo a la traza el nombre del método, pero sería muy sencillo modificarlo para añadir el resto de la información.

Configurando el interceptor

Al principio del post decía que lo bueno de la AOP es que permitía implementar los cross-cutting concerns de forma independiente y luego aplicarlos a las clases que quisiéramos. Esto en Castle Windsor se realiza mediante la configuración de los componentes que registramos en el contenedor.

Lo primero que debemos hacer es registrar en el contenedor el tipo del propio interceptor, ya que cuando Windsor vaya a crear un interceptor lo resolverá del contenedor, cosa que nos permite además añadir al interceptor dependencias sobre otros servicios registrados en el contenedor, como en este caso que depende de ITraceWriter.

var container = new WindsorContainer();
container.Register(Component.For<TraceInterceptor>());

Al registrar los componentes que queremos interceptar, debemos indicar qué interceptores queremos aplicarles:

container.Register(Component.For<ICalculator>()
                            .ImplementedBy<Calculator>()
                            .Interceptors(new InterceptorReference(typeof (TraceInterceptor)))
                            .Anywhere);

Aunque en el ejemplo anterior lo estamos haciendo para un componente individual (ICalculator), podemos aplicar cualquier convención usando container.Register(AllTypes.FromAssembly...) para configurar el interceptor en todos los componentes que queramos.

Una cosa a tener en cuenta es que al configurar el interceptor podemos elegir el orden en que se va a ejecutar dentro de la cadena de inteceptores que hallamos definido. Usando el fluent interface podemos usar las propiedades AnywereFirst o Last para añadir el interceptor en cualquier parte, al principio o al final de la cadena, respectivamente.

Decidiendo qué métodos interceptar

La forma que acabamos de ver de configurar un interceptor lo hace de tal forma que todos los métodos en son interceptados. Hay ocasiones en que esto no es deseable y existen para ello varias soluciones.

La solución más directa sería poner un if al principio de nuestro código en el interceptor para decidir, en función del nombre del método, si queremos ejecutar la lógica de intercepción o no. Lo malo de esta solución es que ensucia el código del interceptor y acopla la decisión de si hay que interceptar o no a la intercepción como tal.

Windsor nos permite hacer esto de una forma mucho más limpia y efectiva, separando la decisión de los métodos a interceptar de la implementación del interceptor. Para ello debemos implementar el interface IProxyGenerationHook. Este interface nos permite, entre otras cosas, los métodos interceptados:

public class MethodSelectorProxyGenerationHook :IProxyGenerationHook
{
    private readonly string[] methodsToIntercept;

    public MethodSelectorProxyGenerationHook(params string[] methodsToIntercept)
    {
        this.methodsToIntercept = methodsToIntercept;
    }

    public void MethodsInspected()
    {
    }

    public void NonProxyableMemberNotification(Type type, MemberInfo memberInfo)
    {
    }

    public bool ShouldInterceptMethod(Type type, MethodInfo methodInfo)
    {
        return methodsToIntercept.Contains(methodInfo.Name);
    }
}

En este ejemplo sencillo, indicamos en el constructor los nombres de los métodos que deben ser interceptados y luego, en el método ShouldInterceptMethod, decimos que sólo debemos interceptar un método si está dentro de la lista de métodos a interceptar. En un caso más real, podríamos basarnos en un atributo, un espacio de nombres, un valor de retorno o cualquier otra convención para decidir si el método ha de ser interceptado o no.

Este patrón de separar la decisión de la acción es algo muy recomendable y, por suerte, muy frecuente en todo el código de Castle. Permite una flexibilidad enorme y a la vez genera un código muy claro.

Limitaciones

Como dije antes, los interceptores se aplican usando un proxy generado dinámicamente. Esta generación tiene un impacto la primera vez que se genera el interceptor. Afortunadamente DynamicProxy, el encargado de generar los proxies, tiene un sistema de cache que funciona muy bien y después de generar el proxy para una clase por primera vez, el resto de las veces no hay penalización al rendimiento. De todas formas, como en todos los temas de rendimiento, lo mejor es medir qué impacto tiene en la aplicación antes de empezar a optimizar nada.

Otras librerías de AOP como PostSharp no tienen este problema porque inyectan los aspectos de forma estática en una fase post-compilación usando técnicas de IL weaving que modifican directamente el assembly generado añadiendo las instrucciones IL necesarias.

Además, debe tenerse en cuenta que sólo se interceptan los métodos que pertecenen al interface con el que hemos registrado el componente. Castle genera un proxy que implementa el interface y redirige las llamadas a la implementación que hemos registrado después de pasar por el interceptor. Eso quiere decir que el resto de los métodos de la implementación no serán interceptados. No debería ser problema porque el interface es el contrato externo del servicio y, por tanto, el punto de entrada a cualquier funcionalidad expuesta.

Conclusiones

El ejemplo que hemos visto es seguramente de los más típicos, pero el uso de interceptores para implementar AOP permite hacer cosas realmente interesantes:

  • Aplicar restricciones de seguridad al invocar métodos dependiendo del usuario actual de la aplicación.
  • Implementar una política de reintentos en caso de que las llamadas a ciertos métodos fallen (imaginad un servicio externo que puede estar caído).
  • Usar un sistema de cache para ciertos métodos cuya ejecución resulte costosa.

El código completo del ejemplo lo podéis encontrar en mi cuenta de github.

4 comentarios en “AOP con Castle Windsor: IInterceptor

  1. Pingback: Tratar con Proxies en Castle Windsor « Koalite's blog

  2. Pingback: AOP con Castle Windsor: DynamicInterceptionFacility

  3. Pingback: Duck Typing en C# con Castle.DynamicProxy

Comentarios cerrados.