Un Framework para Programación Orientada a Aspectos (AOP) en C#    Parte 1: Implementación

Esta publicación es la segunda parte (aunque diga parte uno) de una serie de cuatro publicaciones que estaré realizando sobre programación orientada a aspectos en .NET empleando el lenguaje de programación C#.

Hace un tiempo, trabajando con Diego Herrera(@vermicida) decidimos indagar un poco dentro del mundo del AOP y crear un framework que de forma universal permitiera a cualquier desarrollo en .NET crear pequeñas librerías que sirvieran para satisfacer una funcionalidad contextual como por ejemplo el registro de eventos (logging), la gestión de excepciones (exception handling), mecanismos de seguridad (como autenticación continua con cada invocación a métodos) o monitoreo de desempeño del sistema (performance monitoring).

Nuestra principal necesidad al crear este framework era que los desarrolladores tuviesen las herramientas necesarias para aplicar las diferentes funcionalidades (aspects) transversales (cross cutting) de manera eficiente, sin resultar una carga que relentizara el tiempo de implementación pero que a la ves, desde un punto de gobernabilidad del proceso de desarrollo, no fueran capaces de omitir la colocación de dicha funcionalidad, es decir, evitar por ejemplo el típico caso de “como son muchas cosas se me pasó escribir el código que loggea eventos”, ó “Es que no sabía que colocar”.

El mecanismo elegido es el uso de atributos de no más de dos (2) parámetros como máximo y de aplicación sobre métodos, de forma tal que los desarrolladores sólo tengan que colocar el atributo correspondiente al aspecto que se desea aplicar en los métodos en los que sea requerido, siendo la configuración del aspecto establecida por parámetros. Estos parámetros tendrán un valor por defecto lo suficientemente significativo para el aspecto para cubrir aquellos casos en los que los desarrolladores los omitan. Un ejemplo de un programa que emplearía este framework sería el siguiente:

...
    public class TestObject : InterceptableObject
    {
        [Log(VerbosityLevel.None)]
        public void MethodA() { }

        [Log(VerbosityLevel.Light)]
        public void MethodB(string str) { }

        [Log(VerbosityLevel.Medium)]
        public string MethodC(string name)
        {
            return String.Format(@"Hola {0}", name);
        }
        
        ...
    }
...

En el código anterior, el atributo Log es un aspecto desarrollado empleando este framework.

Al final del día lo que buscábamos era un mecanismo eficiente, simple, sencillo, y que ofreciera al equipo de desarrollo la posibilidad de agregar las funcionalidades transversales, evitando desvíos en la gobernabilidad de las desiciones y conceptos de diseño de la arquitectura (como el formato de los mensajes de excepción por ejemplo).

Implementación

La siguiente imagen corresponde al diagrama de clases de este framework y cuyos elementos iremos describiendo y detallando a lo largo de esta publicación:

Diagrama de Clases

Haz click para ver la imagen más grande.

Recordando de la parte cero de esta serie de publicaciones, en .NET para que una clase sea interceptable debe cumplir con dos (2) condiciones:

  1. Debe extender de la clase ContextBoundObject
  2. Debe estar decorada con un atributo que extienda de ContextAttribute

Un objeto que extiende de ContextBoundObject es como el Buda, adquiere conciencia de sí mismo, de su relación con el universo (el CLR) y con los demás. Esto es muy útil porque podemos de alguna manera saber cuando se va a ejecutar un método dentro del objeto y cuando dicho método a finalizado su ejecución, que es justamente lo que se busca hacer en AOP para interceptar e intervenir la ejecución de los métodos e inyectar la funcionalidad del aspecto que se quiere introducir.

Sin embargo, un ContextBoundObject por sí sólo es incapaz de procesar la inyección en sí de código para los aspectos, sólo es capaz de decir cuando un método es invocado o retorna de su invocación; de ahí que se tenga que decorar con un ContextAttribute, que agregará a la meta-data de la clase toda la información y mecanismos necesarios para interactuar con su contexto, lo que en el caso de este framework permitirá redirigir la ejecución a las porciones de funcionalidad de los aspectos a inyectar.

Pero en una arquitectura de N-capas con todo un dominio de clases susceptibles de ser interceptadas, podemos encontrarnos con que es realmente incómodo, poco escalable y flexible el tener que hacer que (todas) las clases de la arquitectura que requieran intercepción extiendan de ContextBoundObject, y que estén decoradas con el atributo ContextAttribute. Puede pasar que a algún desarrollador se le olvide extender de ContextBoundObject o aplicar el atributo ContextAttribute, y una de las intenciones de este framework es evitar que estas situaciones pasen.

Lo ideal es que las clases de la arquitectura extiendan de una única clase padre, que sea la que esté decorada con el atributo ContextAttribute y que extienda de ContextBoundObject, y que llamaremos InterceptableObject:

using System;
using System.Security.Permissions;

namespace AOPFramework
{
    /// <summary>
    /// Represents a context-bound object whose execution must be intercepted.
    /// </summary>
    [InterceptContextAttribute]
    public abstract class InterceptableObject : ContextBoundObject
    {
        /// <summary>
        /// Class constructor.
        /// </summary>
        protected InterceptableObject() : base() { }

        /// <summary>
        /// Obtains a lifetime service object to control the lifetime policy for this instance.
        /// </summary>
        /// <returns>Returns <c>null</c> since a lifetime service is not defined for interceptable objects.</returns>
        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.Infrastructure)]
        public override object InitializeLifetimeService()
        {
            return null;
        }
    }
}

Hemos definido esta clase como abstracta para impedir que pueda ser instanciada, obligando a todas las clases de la arquitectura consumidora de este framework a tener que extender de ella y de esta forma generar una familia de clases que podría ser identificadas de forma unívoca como “interceptables”.

Igualmente puede apreciarse que la clase InterceptableObject está decorada con un attributo de tipo InterceptContextAttribute, que no es más que una especificación de la clase ContextAttribute:

using System;
using System.Runtime.Remoting.Activation;
using System.Runtime.Remoting.Contexts;
using System.Security.Permissions;

namespace AOPFramework
{
    /// <summary>
    /// Defines an attribute that will decorate those classes that can be intercepted in order to process its behaviour, represented by
    /// the invokation of its methods.
    /// </summary>
    /// <remarks>
    /// Class extends from <c>ContextAttribute</c>, which provides the default implementations of the <c>IContextAttribute</c> and <c>IContextProperty</c> interfaces.
    /// </remarks>
    [AttributeUsage(AttributeTargets.Class, Inherited = true)]
    internal sealed class InterceptContextAttribute : ContextAttribute
    {
        /// <summary>
        /// Class constructor.
        /// </summary>
        public InterceptContextAttribute() : base(Guid.NewGuid().ToString()) { }

        /// <summary>
        /// Called when the context is frozen.
        /// </summary>
        /// <param name="newContext">The context to freeze.</param>
        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.Infrastructure)]
        public override void Freeze(Context newContext) { }

        /// <summary>
        /// Adds the current context property to the given message.
        /// </summary>
        /// <param name="ctorMsg">The <c>IConstructionCallMessage</c> to which to add the context property.</param>
        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.Infrastructure)]
        public override void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg)
        {
            if (ctorMsg != null)
            {
                ctorMsg.ContextProperties.Add(new InterceptProperty(this.Name));
            }
        }

        /// <summary>
        /// Returns a Boolean value indicating whether the context parameter meets the context attribute's requirements.
        /// </summary>
        /// <param name="ctx">The context in which to check.</param>
        /// <param name="ctorMsg">The <c>IConstructionCallMessage</c> to which to add the context property.</param>
        /// <returns>
        /// This method will return <c>true</c> if the passed in context is okay; otherwise, it will return <c>false</c>.
        /// </returns>
        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.Infrastructure)]
        public override bool IsContextOK(Context ctx, IConstructionCallMessage ctorMsg)
        {
            return (ctx != null) && (ctx.GetProperty(this.Name) as InterceptProperty) != null;
        }

        /// <summary>
        /// Returns a Boolean value indicating whether the context property is compatible with the new context.
        /// </summary>
        /// <param name="newCtx">The new context in which the property has been created.</param>
        /// <returns>
        /// This method will return <c>true</c> if the context property is okay with the new context; otherwise, it will return <c>false</c>.
        /// </returns>
        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.Infrastructure)]
        public override bool IsNewContextOK(Context newCtx)
        {
            return (newCtx != null) && (newCtx.GetProperty(this.Name) as InterceptProperty) != null;
        }
    }
}

Como la idea es crear un framework, y parte de ello significa esconder la mayor cantidad de complejidad a futuros consumidores, la clase InterceptContextAttribute se implementa como internal para que no pueda ser referenciada fuera del assembly del framework y sea empleada únicamente por la clase InterceptableObject.

Nuevamente recordando de la parte cero de esta serie de publicaciones, en .NET cuando se trabaja con clases de contexto, existe el concepto de sumidero (o sink): clases responsables de gestionar los mensajes que genera la clase, determinando la apropiada y correspondiente inyección de aspectos. La clase InterceptProperty, que es referenciada en la anterior InterceptContextAttribute, es responsable de comunicar al contexto el tipo (Type) correspondiente al sumidero que gestionará los mensajes que se generen.

using System;
using System.Runtime.Remoting.Contexts;
using System.Runtime.Remoting.Messaging;

namespace AOPFramework
{
    /// <summary>
    /// This class implements the necessary methods to represent a property that can be intercepted and provide information as well as
    /// data to the context sink.
    /// </summary>
    internal class InterceptProperty : IContextProperty, IContributeClientContextSink, IContributeServerContextSink
    {
        /// <summary>
        /// Class constructor.
        /// </summary>
        public InterceptProperty(string name) : base() { this.Name = name; }

        /// <summary>
        /// Gets the name of the property under which it will be added to the context.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Called when the context is frozen.
        /// </summary>
        /// <param name="newContext">The context to freeze.</param>
        public void Freeze(Context newContext) { }

        /// <summary>
        /// Returns a Boolean value indicating whether the context property is compatible with the new context.
        /// </summary>
        /// <param name="newCtx">The new context in which the ContextProperty has been created.</param>
        /// <returns>
        /// <c>True</c> if the context property can coexist with the other context properties in the given context; otherwise, <c>false</c>.
        /// </returns>
        public bool IsNewContextOK(Context newCtx)
        {
            return (newCtx != null) && (newCtx.GetProperty(this.Name) as InterceptProperty) != null;
        }

        /// <summary>
        /// Chains the message sink of the provided server object in front of the given sink chain.
        /// </summary>
        /// <param name="nextSink">The chain of sinks composed so far.</param>
        /// <returns>The composite sink chain.</returns>
        public IMessageSink GetClientContextSink(IMessageSink nextSink)
        {
            return new InterceptSink(nextSink);
        }

        /// <summary>
        /// Chains the message sink of the provided server object in front of the given sink chain.
        /// </summary>
        /// <param name="nextSink">The chain of sinks composed so far.</param>
        /// <returns>The composite sink chain.</returns>
        public IMessageSink GetServerContextSink(IMessageSink nextSink)
        {
            return new InterceptSink(nextSink);
        }
    }
}

Nótese que la clase implementa tres (3) interfaces:

Ahora viene la parte complicada en el diseño del framework. Independientemente de si el mensaje entra o sale del contexto, la clase InterceptProperty retornará una instancia de la clase InterceptSink, que será el único sumidero que interese para procesar los mensajes. Es decir, que todos los mensajes serán procesados por el mismo sumidero independientemente del momento de su intercepción.

Estos mensajes son producidos por la invocación o retorno de los métodos de las clases que extiendan de InterceptableObject, los cuales se espera estén decorados con atributos que extienden de la clase InterceptableAttribute, y que permite emplear este framework para crear atributos custom (tal y como se mostró en la primera porción de código al inicio de esta publicación) que servirán para indicar en el código fuente la inyección de un aspecto.

using System;

namespace AOPFramework
{
    /// <summary>
    /// Defines or decorates a class as interceptable. An interceptable class can be processed by a class that implements the <c>IProcessor</c> interface.
    /// </summary>
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public abstract class InterceptableAttribute : Attribute
    {
        /// <summary>
        /// Sets or gets the processor for this class.
        /// </summary>
        public IProcessor Processor { get; set; }

        /// <summary>
        /// Sets or gets this attribute processing priority.
        /// </summary>
        /// <remarks>
        /// The bigger the value of this property, the bigger the importance or priority of processing
        /// this attribute.
        /// </remarks>
        public int Priority { get; set; }
    }
}

El atributo InterceptableAttribute permite agregar dos propiedades a la meta-data de los métodos que se decoren con él: una instancia de la interface IProcess y un valor entero que define la prioridad de atención al aspecto que se modele con este atributo.

Un Tema de Prioridad

El argumento detrás de una propiedad para definir una prioridad es que en .NET al recuperar los atributos (por reflection por ejemplo), estos aparecen en el órden inverso a como fueron colocados en el código. Esto significa que los programadores estarían restringidos a colocar los atributos que definen aspectos en el órden preciso que se necesita para su inyección, lo cual primero no es flexible y segundo puede llevar a escenarios que rompan con la gobernabilidad que haya decidido el Arquitecto de Software o el Líder Técnico para la inyección de los aspectos.

Propiedad Prioridad

Definiendo una prioridad para un aspecto como parte de sus propiedades y ordenando luego los aspectos que se deben aplicar a un método acorde a dicha prioridad se otorga a los programadores la libertad de agregar los atributos que los definen en el orden que deseen. Más aún, si durante la vida de un proyecto se crean o descartan aspectos, sólo bastará con agregar o remover el atributo correspondiente sin tomar en cuenta si esta de primero o último en la pila de atributos (ganando flexibilidad y escalabilidad en el diseño e implementación).

Un Tema de Procesamiento

La otra propiedad que agrega a la meta-data de los métodos decorados con el atributo InterceptableAttribute es la posibilidad de obtener una instancia de la inteface IProcess que definirá el contrato que debe seguir los aspectos que se programen con este framework:

using System;
using System.Runtime.Remoting.Messaging;

namespace AOPFramework
{
    /// <summary>
    /// Defines the interface for those objects that will process the calling and return messages for a class that extends 
    /// from <c>InterceptableObject</c>.
    /// </summary>
    /// <seealso cref="AOPFramework.InterceptableObject"/>
    public interface IProcessor
    {
        /// <summary>
        /// Process a <c>Method Call Message</c>.
        /// </summary>
        /// <remarks>
        /// This method will be called before the execution of the body of an interceptable or processable object.
        /// </remarks>
        /// <param name="callMessage">The <c>Method Call Message</c> to process.</param>
        /// <param name="processable">The processable decorated object to process.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage(@"Microsoft.Naming", @"CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = @"processable", Justification = @"According to Merriam-Webster dictionary, the word 'Processable' exists and is correctly spelled (http://www.merriam-webster.com/dictionary/processable).")]
        void ProcessCallMessage(IMethodCallMessage callMessage, InterceptableAttribute processable);

        /// <summary>
        /// Process a <c>Method Return Message</c>.
        /// </summary>
        /// <remarks>
        /// This method will be called after the execution of the body of an interceptable or processable object. The <c>returnMessage</c> parameter will
        /// contain the result of the execution, including any possible exception type.
        /// </remarks>
        /// <param name="returnMessage">The <c>Method Return Message</c> to process.</param>
        /// <param name="processable">The processable decorated object to process.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage(@"Microsoft.Naming", @"CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = @"processable", Justification = @"According to Merriam-Webster dictionary, the word 'Processable' exists and is correctly spelled (http://www.merriam-webster.com/dictionary/processable).")]
        void ProcessReturnMessage(IMethodReturnMessage returnMessage, InterceptableAttribute processable);
    }
}

Esta interface define dos métodos:

  • ProcessCallMessage: Establece el método que debe atender el pre-procesamiento o acciones a realizar antes de ejecutar el método interceptado. Básicamente es el que sería invocado en el momento en que un ContextAttribute procede a ejecutar (llamar) a uno de sus métodos, inyectando aspectos cuya ejecución se realizará previa a la ejecución de dicho método.
  • ProcessReturnMessage: Establece el método que debe atender el post-procesamiento o acciones a realizar después de haber ejecutado el método que está siendo interceptado; es decir, inyecta aspectos cuya ejecución se realizará posterior a la ejecución de dicho método.

Retomando el Sumidero

Como se explicó previamente, la clase InterceptProperty retornará una instancia de la clase InterceptSink, que en este framework es el único sumidero necesario para atender los mensajes entrantes y salientes.

using System;
using System.Runtime.Remoting.Messaging;

namespace AOPFramework
{
    /// <summary>
    /// Represents  an intercept sink.
    /// </summary>
    internal class InterceptSink : IMessageSink
    {
        /// <summary>
        /// Stores the next message sink in the sink chain.
        /// </summary>
        private IMessageSink nextSink;

        /// <summary>
        /// A generic instance of an object to lock on, rather than locking on the type itself, to avoid deadlocks.
        /// </summary>
        /// <remarks>
        /// The initialization of this variable uses the <i>static initialization</i> approach, where the instance is created 
        /// the first time any member of the class is referenced. The common language runtime (CLR) takes care of the variable 
        /// initialization.
        /// </remarks>
        private static object syncRoot = new Object();

        /// <summary>
        /// Class constructor.
        /// </summary>
        /// <param name="nextSink">The next message sink in the sink chain.</param>
        public InterceptSink(IMessageSink nextSink)
        {
            lock (syncRoot)
            {
                this.nextSink = nextSink;
            }
        }

        /// <summary>
        /// Gets the next message sink in the sink chain.
        /// </summary>
        public IMessageSink NextSink
        {
            get { return this.nextSink; }
        }

        /// <summary>
        /// Asynchronously processes the given message.
        /// </summary>
        /// <param name="msg">The message to process.</param>
        /// <param name="replySink">The reply sink for the reply message.</param>
        /// <returns>
        /// Returns an <c>IMessageCtrl</c> interface that provides a way to control asynchronous messages after they have been dispatched.
        /// </returns>
        public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)
        {
            return nextSink.AsyncProcessMessage(msg, replySink);
        }

        /// <summary>
        /// Synchronously processes the given message.
        /// </summary>
        /// <param name="msg">The message to process.</param>
        /// <returns>A reply message in response to the request.</returns>
        public IMessage SyncProcessMessage(IMessage msg)
        {
            IMethodCallMessage callMessage = (msg as IMethodCallMessage);

            if (callMessage != null)
            {
                InterceptableAttribute[] processables = (InterceptableAttribute[])(msg as IMethodMessage).MethodBase.GetCustomAttributes(typeof(InterceptableAttribute), true);
                
                // Sort 'aspects' (attributes) based on the value of the 'priority' property.
                Array.Sort<InterceptableAttribute>(processables, new Comparison<InterceptableAttribute>(delegate(InterceptableAttribute a, InterceptableAttribute b) { return a.Priority.CompareTo(b.Priority); }));

                // Do pre-processing: process the method call before its body execution. The calling parameters are going to be available.
                for (int i = 0; i < processables.Length; i++)
                {
                    processables[i].Processor.ProcessCallMessage(callMessage, processables[i]);
                }

                // Do post-processing: process the method call after its body execution. The return values (or exception) are going to be available.
                IMethodReturnMessage returnMessage = (nextSink.SyncProcessMessage(callMessage) as IMethodReturnMessage);

                if (returnMessage != null)
                {
                    for (int i = 0; i < processables.Length; i++)
                    {
                        processables[i].Processor.ProcessReturnMessage(returnMessage, processables[i]);
                    }

                    return returnMessage;
                }
            }

            return msg;
        }
    }
}

El método SyncProcessMessage es el principal responsable en esta clase, y se encarga de ejecutar las siguientes acciones con cada instancia de IMessage que procesa:

  1. Se verifica que el mensaje es un mensaje de llamada (call) a un método. Es decir, que el mensaje es una instancia de la interface IMethodCallMessage.
  2. Se recuperan todos los atributos del mensaje que sean del tipo InterceptableAttribute.
  3. Se ordenan los atributos acorde a su propiedad de prioridad.
  4. Se obtiene la instancia de la clase procesadora del aspecto (de tipo IProcessor) y se invoca al método ProcessCallMessage para inyectar cualquier funcionalidad que defina el aspecto y que deba ejecutarse previo a la ejecución del cuerpo del método que está siendo interceptado.
  5. Se procede a continuar con la cadena (chain) conceptual de funcionalidades; verificando que los mensajes que se reciben sean de salida (return) de un método. Es decir, que el mensaje es una instancia de la interface IMethodReturnMessage.
  6. Nuevamente se obtiene la instancia de la clase procesadora del aspecto, y se invoca al método ProcessReturnMessage para inyectar cualquier funcionalidad que defina el aspecto y que deba ejecutarse posteriormente a la ejecución del cuerpo del método que está siendo interceptado.
  7. Se retorna el mensaje para continuar con la cadena conceptual.

Finalizando

La siguiente imagen corresponde al diagrama de dependencias de clases de este framework y que muestra la relación de los elementos descritos y detallados en esta publicación:

Haz click para ver la imagen más grande.

Para emplear este framework y crear aspectos se deben seguir los siguientes pasos:

  1. Crear un atributo para definir el aspecto (como el atributo Log del ejemplo al principio de esta publicación) simplemente extendiendo la clase InterceptableAttribute. A esta implementación se le pueden agregar tantas propiedades como sean necesarias para definir la meta-data y funcionalidades del aspecto.
  2. Implementar la interface IProcessor y sus dos métodos (si así se requiere) para inyectar la funcionalidad del aspecto en los métodos interceptados.

En próximas publicaciones mostraré ejemplos de como emplear este framework para crear aspectos y consumirlos dentro de un diseño.

Limitaciones

Emplear y diseñar arquitecturas de software empleando el paradigma de AOP no es ni una panacea ni una utopía en .NET; lamentablemente, existe un precio a pagar en tiempo de ejecución y de memoria entre otras limitaciones que debemos tener en cuenta:

  • Desempeño: Empleando diferentes herramientas de medición de desempeño, he encontrado que en el peor caso puede haber un incremento de tiempo de ejecución en cada llamada (invocación a método interceptado) de cerca de 0,05 microsegundos. Esto quiere decir que se necesita cerca de unas 20000 llamadas para perder un segundo, lo cual se podría considerar no tan grave. Sin embargo, en aplicativos donde el tiempo y el desempeño sea primordial… quizás el enfoque de AOP no sea el más apropiado.
  • Memoria: El uso de contextos propios, así como de sinks y en general cualquier objeto que extienda de la clase ContextBoundObjects consume más memoria que objetos “normales”. Hoy día el recurso de memoria no suele ser considerado como un recurso costoso o valioso, pero siempre es importante tenerlo en cuenta.
  • Capacidades: Lamentablemente por diseño y arquitectura del CLR es, a través de esta aproximación e implementación de AOP, absolutamente imposible interceptar métodos o elementos estáticos (static) del código. Esto impone una importante limitación a la hora de diseñar arquitecturas de software, porque significa que o bien descartamos el uso de elementos estáticos, o bien asumimos que su criticidad y necesidad de ser interceptados puede ser ignorada de forma segura, o que su operatividad y la inyección de aspectos puede inferirse y extrapolarse desde sus miembros llamadores no estáticos.

Código Fuente

Por último, haciendo click aquí podrán conseguir una copia del código fuente completo descrito en esta publicación.

Referencias

Anuncios

Un Framework para Programación Orientada a Aspectos (AOP) en C#    Parte 0: Introducción

Este es el primero de una serie de cuatro publicaciones que estaré realizando sobre programación orientada a aspectos en .NET empleando el lenguaje de programación C#.
Mi intención inicial era que la primera parte incluyese una apropiada introducción teórica a la programación orientada a aspectos, adicional a los detalles propios de diseño e implementación de un framework. Sin embargo, la longitud que estaba adquiriendo esta primera publicación me ha obligado a dividirla en dos partes: una parte cero o introductoria con los conceptos y elementos teóricos, y una futura parte uno con el correspondiente detalle técnico de implementación.

Acorde a la Wikipedia la programación orientada a aspectos, o AOP por sus siglas en inglés, es un paradigma de programación cuya intención es permitir una adecuada modularización de las aplicaciones y posibilitar una mejor separación de incumbencias; encapsulando de forma efectiva los diferentes conceptos que componen una aplicación en entidades bien definidas, eliminando las dependencias entre cada uno de ellas.

El principal objetivo de AOP es la separación de las funcionalidades dentro del sistema:

  • Por un lado funcionalidades comunes utilizadas a lo largo de la aplicación.
  • Por otro lado, las funcionalidades propias de cada módulo.

Cada funcionalidad común se encapsulará en una entidad.

Origen y Razón de Ser

La idea de la programación orientada a aspectos surge directamente del paradigma de la programación orientada a objectos (o OOP por sus sigas en inglés), en la cual muchas veces al estar implementando funcionalidades de un aplicativo de tamaño considerable empiezan a aparecer retos y problemas que no son solventables a través de las formas o técnicas habituales. Algunos de estos problemas son:

  • Scattered Code: o código disperso, no es más que líneas de código que están distribuidas por toda o gran parte del aplicativo, y que incrementan de forma considerable la dificultad de las tareas de mantenimiento y desarrollo.
  • Tangled Code: o código enmarañado, es un problema que puede aparecer cuando una misma pieza de código (como un módulo) implementa múltiples comportamientos o aspectos diferentes del sistema de forma simultánea, empleando cláusulas de tipo switch ó if-then-else anidadas.
  • Loss Uniformity: o pérdida de uniformidad, es más un problema de forma y gobernabilidad del desarrollo, que de diseño o implementación. Se basa en que aún cuando el arquitecto de software o el líder técnico haya sentado de manera formal los estándares y convenios de implementación en temas como nomenclatura o mensajes, estos se pierden a lo largo del ciclo de vida del desarrollo del aplicativo. El caso más común son los mensajes de la aplicación para casos como por ejemplo registro de evetos o excepciones, donde por más que exista un lineamiento formal, cada desarrollador puede acabar escribiendo el mensaje acorde a su mejor sentido común. Este fenómeno se encuentra mucho en equipos de desarrollo de alta rotación, en proyectos de duración muy prolongada (superior a dos años) o que son mantenidos por equipos de desarrollo diferentes.

Al final del día, la razón de ser de una AOP es buscar minimizar (o solventar) el desorden que puede aparecer en la arquitectura de un aplicativo desarrollado mediante OOP. Este desorden aparece cuando algo que el sistema tiene que hacer requiere la participación de muchos y variados objetos diferentes, lo cual suele ocurrir por la abstracción de los requerimientos en objetos que realiza el diseñador, quien a menudo pierde de vista el hecho de que en última instancia, dichos objetos van interactuar unos con otros con el fin de cumplir con tales requerimientos.

Lo anterior ocurre con facilidad y de forma subconscientemente, porque la forma fundamental en que interactúan los requerimientos es muchas veces diferente de la forma en que interactúan los objetos.

Los conceptos descritos en los patrones de diseño son un intento de entender formalmente los temas de la interacción de objetos y para proporcionar al diseñador de algunos medios bien comprendidos para resolver este problema de interdependencia. Más aún, la mayoría de las implementaciones de un AOP se apoyan fuertemente en el patrón de diseño observador (u Observer en inglés).

Así, podemos decir a modo de resumen que AOP surge como un paradigma de programación dentro del mismo paradigma de OOP, que no busca sustituir a éste, sino más bien mitigar algunas de sus limitaciones al modularizar todo aquel código de índole transversal en un sistema de software.

Por supuesto que AOP no es la panacea o un hito utópico en la implantación de arquitecturas y en el desarrollo de aplicativos en OOP. Siempre se necesitará de la participación de equipos ágiles de desarrollo, con credenciales, entrenamiento y formación apropiada, experiencia adecuada y un liderazgo eficaz para llevar con éxito un proyecto de desarrollo de software, con o sin el seguimiento de una AOP.

Terminología

Los siguientes son los conceptos que se suelen manejarse en una AOP (según han sido tomados desde la Wikipedia):

  • Aspect (Aspecto) es una funcionalidad transversal (cross-cutting) que se va a implementar de forma modular y separada del resto del sistema, y que en una OOP tradicional, es el causante habitual de código repetido, disperso o enmarañado. El ejemplo más común y simple de un aspecto es el registro de eventos (event logging) dentro del sistema, ya que necesariamente afecta a todas las partes que generan un suceso.
  • Join point (Punto de Cruce o de Unión) es un punto de ejecución dentro del sistema donde un aspecto puede ser introducido, como la invocación o retorno de un método, la ocurrencia de una excepción o la modificación de un campo. El código del aspecto será inyectado en el flujo de ejecución de la aplicación para añadir su funcionalidad.
  • Advice (Consejo) es la implementación del aspecto, es decir, contiene el código que implementa la nueva funcionalidad. Se insertan en la aplicación en los Puntos de Cruce.
  • Pointcut (Puntos de Corte) define los Consejos que se aplicarán a cada Punto de Cruce. Se especifica mediante Expresiones Regulares o mediante patrones de nombres (de clases, métodos o campos), e incluso dinámicamente en tiempo de ejecución según el valor de ciertos parámetros.
  • Introduction (Introducción) permite añadir métodos o atributos a clases ya existentes. Un ejemplo en el que resultaría útil es la creación de un Consejo de Auditoría que mantenga la fecha de la última modificación de un objeto, mediante una variable y un método apropiado a la tarea, que podrían ser introducidos en todas las clases (o sólo en algunas) para proporcionarles esta nueva funcionalidad. Este concepto también es conocido como inyección de código, o injection por su término en inglés.
  • Target (Destinatario) es la clase aconsejada, la clase que es objeto de un consejo. Sin AOP, esta clase debería contener su lógica, además de la lógica del aspecto.
  • Proxy (Resultante) es el objeto creado después de aplicar el Consejo al Objeto Destinatario. El resto de la aplicación únicamente tendrá que soportar al Objeto Destinatario (pre-AOP) y no al Objeto Resultante (post-AOP).
  • Weaving (Tejido) es el proceso de aplicar Aspectos a los Objetos Destinatarios para crear los nuevos Objetos Resultantes en los especificados Puntos de Cruce. Este proceso puede ocurrir a lo largo del ciclo de vida del Objeto Destinatario y del desarrollo del aplicativo:
    • Aspectos en Tiempo de Compilación, que necesita un compilador especial.
    • Aspectos en Tiempo de Carga, los Aspectos se implementan cuando el Objeto Destinatario es cargado a memoria. Requiere un ClassLoader especial.
    • Aspectos en Tiempo de Ejecución.

Contextos en .NET

Cuando entramos en el mundo de .NET y de AOP es necesario explicar un concepto adicional: el de Contextos. En .NET, un contexto es un conjunto de propiedades o reglas de uso que definen un entorno en el que una colección de objetos reside. Las reglas se aplican cuando los objetos entran o salen de un contexto.

Los contextos se crean durante la activación de un objeto. Un nuevo objeto se coloca en un contexto existente o en un nuevo contexto creado con los atributos incluidos en la metadata del tipo que corresponde al objeto activado.

Desde el punto de vista del CLR (Common Lenguage Runtime), un contexto no es más que una de las muchas subdivisiones que puede llegar a tener un app domain. Más aún, cuando un app domain es creado, tiene un contexto por defecto (que justamente se llama default context).

Un contexto funciona básicamente a través de mensajes (objetos que implementan la interfaz IMessage), que se definen como un conjunto de propiedades tales como el nombre del método que se está invocando, sus parámetros (tanto de entrada como de salida) así como cualquier otra característica propia que defina a la invocación. Estos mensajes son recibidos por sumideros (o sinks en inglés, y que son objetos que implementan la interfaz IMessageSink) y encargadas de procesar los mensajes para ejecutar acciones a través de los datos que estos portan.

La idea de contexto es fundamental para desarrollar una AOP en .NET empleado el lenguage C#, ya que es gracias a las clases que permiten decorar objectos como objetos de contexto (tal como ContextBoundObject) que se podrá intervenir en la ejecución de la lógica de éstos para inyectar la funcionalidad de los aspectos que se implementen a través de los mensajes.

Cuando se ejecuta un sistema desde la óptica de la clase ContextBoundObject ocurre lo siguiente:

  1. La invocación a un método se convierte en una instancia de IMessage.
  2. El mensaje pasa a través de una serie de sumideros (objetos del tipo IMessageSink), que son enlazados para formar una cadena (chain) conceptual.
  3. Cada sumidero es capaz de analizar el mensaje y sus características, ejecutando una lógica específica.
  4. De ser necesario, un sumidero puede modificar el mensaje.
  5. Cuando un sumidero finaliza de procesar el mensaje, lo pasa al siguiente sumidero en la cadena conceptual.
  6. Eventualmente la cadena es totalmente ejecutada y el método invocado se ejecuta.
  7. Si el método retorna algún valor (o genera una excepción) la cadena de sumideros se ejecuta nuevamente en sentido revertido.

Un Framework para .NET

Si realizamos una búsqueda en Internet sobre frameworks en .NET para realizar AOP, encontramos muchos productos que ofrecen toda una gama de posibilidades para inyectar contextos dentro del código durante el tiempo de diseño/codificación del sistema, durante su compilación o después de ésta directamente en el MSIL.

Entonces… ¿por qué crear un framework para AOP habiendo tantos productos disponibles?

Pues porque como Arquitecto de Soluciones de Software encuentro siempre reconfortante el poder crear mis propios frameworks sobre los cuales puedo tener un control más fino y delicado, y en los que puedo poner toda mi confianza en que estoy entregando a clientes y compañeros de trabajo productos de calidad que pueden utilizar y extender fácilmente.

Por otra parte, simplemente ocurre que no me convencen los productos que hay actualmente para hacer AOP; no porque sean malos… sino simplemente porque no siguen una aproximación con la que me sienta cómodo, sobre todo porque estos productos suelen ser muy mastodónticos y complejos. De ahí esta iniciativa de crear un framework lo suficientemente minimalista como para ser confortable de emplear y extender, universal para que cada aspecto pueda implementarse independientemente con el apoyo de la tecnología que el desarrollador encuentre más apropiada a sus gustos y necesidades (que en mi caso suele ser apoyarme en los Microsoft Enterprise Libraries).

En la próxima parte de esta serie de publicaciones mostraré los detalles técnicos de diseño e implementación de este custom framework.

Referencias

Disclaimer

Some times, when I want to publish a new software or library just to share it with the world, I found that some sort of disclaimer should be provided. The most common, is one that says something like this:

The Software is provided “as-is” without warranty of any king…. blah… blah…blah

The previous sentence is very common in many open-source licenses like the MIT License and the X11 license, among other DFSG Licenses.

However, sometimes, I just want to have my own simple non-complicated disclaimer for stuff I do and write. Something like:

I do not pretend that this <library_name_goes_here> is the fastest, the leanest, the smallest or the best, I just pretend that it does exactly what I planned it to do, and that it does it well enough for me. I hope it does the same for you, and if it happens that you were able to improve it… just let me know.

Tautología II

balloon graffiti bansky

Los requerimientos de un sistema son como globos en una mano. Si se mantienen fuertemente agarrados se conservará el control y se disfrutarán. Si se sueltan, aunque sea un poco, se safarán y saldrán volando descontroladamente.

Please Stand-By

please_stand_byHoy día es habitual en los aplicativos restringir las posibles acciones de los usuarios cuando el sistema está realizando algún procesamiento. Esto es algo que ha ido evolucionando con el tiempo, pasando de mensajes de texto que titilaban en la pantalla a barras de progresos y animaciones, algunas muy creativas y otras muy molestas.

Con el advenimiento de la tecnología web, surgieron nuevos patrones de comportamiento de los usuarios y nuevos retos para los diseñadores y programadores para evitar que las acciones de éstos afectaran el comportamiento esperado del sistema.

Por ejemplo, uno de los patrones conductuales de los usuarios que eventualmente surgieron, especialmente con los sitios dinámicos y el e-commerce, fue el que se conocería como deja-vù.

Para entender este patrón conductual supongamos el siguiente escenario: un usuario accede a un aplicativo web y pulsa un botón (o un enlace), pero no percibe que el aplicativo esté haciendo algo y nuevamente pulsa el botón. Supongamos que el usuario es tan perceptivo como una oruga y nuevamente cree que el aplicativo no ha recibido su solicitud y se la pasa dándole varias veces al botón hasta que aparece un resultado, el cual muy probablemente sea producto de la primera vez que presionó el botón. En el mejor de los casos hasta allí habrá llegado el problema.

Continuando con el ejercicio, supongamos que el botón se encargaba de insertar alguna información (o más doloroso aún, realiza un debito irreversible a la cuenta bancaria del usuario). Entonces por cada vez que el usuario le dio al botón, el servidor recibió una solicitud de realizar esa acción, que en el mejor caso bien puede ignorar, o puede lanzar un error que el usuario verá a posteriori o simplemente repetirá dicha acción el número de veces que el usuario haya pulsado el botón.

Pero todo esto ocurre porque el aplicativo carecía de un mecanismo para expresar de forma clara y explícita que ha recibido la petición del usuario, más allá de lo que el explorador que esté empleando el usuario haga para indicar que está esperando una respuesta del servidor.

Claro que hoy día los usuarios están más acostumbrados a la latencia de los sistemas web, pero no existen garantías de que el patrón conductual se haya superado al ciento por ciento. Y más si en la actualidad todos los sistemas web están siendo implementados con algún tipo de framework de AJAX que hace que las páginas web no se tengan que cargar completamente para mostrar el resultado de la acción del usuario.

Y para empeorar las cosas, los usuarios ya no quieren el texto de ‘Por favor espere’ titilando en una esquina del monitor, lo que quieren es diseño, animación y color; algo que a veces no tenemos ni el tiempo, ni el dinero o las habilidades para proveer.

Por suerte, existen almas caritativas que recorren el mundo poniendo a su disposición estos tres escasos elementos, para que desarrolladores en general puedan hacer uso de ellos y proveer a los usuarios de sus sistemas de esa barrita que aparece cuando una sombra bloquea toda la pantalla para que no la anden $@#&*!§ por ahí insistiendo en que se haga algo de lo cual el servidor ya está enterado y en vías de procesar.

Y es entonces donde esta entrada les recomienda el siguiente enlace: www.ajaxload.info en el cual se ofrece imágenes tipo gif animados personalizables y de diferentes motivos para agregar como recursos de espera para que los usuarios de nuestros sistemas, hipnotizándolos y entreteniéndolos, no insistan en sus peticiones y simplemente esperen a que el sistema termine de hacer lo que tiene que hacer.

Lo mejor… es totalmente gratis.

Tautología

Iced PC

Desarrollar aplicaciones y andar por sobre el agua son dos cosas muy sencillas; siempre que el agua y los requisitos funcionales esten congelados

Edward V. Berard

Bienvenidos al front-end

The Architect
De la wikipedia extraemos la siguiente definición de front-end:

“El front-end es la parte del software que interactúa con el o los usuarios y el back-end es la parte que procesa la entrada desde el front-end.[…] La idea general es que el front-end sea el responsable de recolectar los datos de entrada del usuario, que pueden ser de muchas y variadas formas, y procesarlas de una manera conforme a la especificación que el back-end pueda usar”.

Se preguntarán, ¿qué tiene que ver una abstracción ampliamente conocida del diseño de software con un blog de arquitectura de soluciones, más allá de lo claramente evidente?

Pues de una simple analogía: que el arquitecto de soluciones de software suele ser el front-end técnico entre lo que el cliente desea y lo que el equipo de trabajo y la tecnología permiten realizar. En otras palabras, se convierte en la interfaz que traduce los deseos del cliente en especificaciones técnicas que su equipo de trabajo puede llevar a cabo con facilidad y efectividad, y retornar el resultado de dicho proceso nuevamente al cliente.

Como todo front-end, ofrece una serie de prestaciones que le hacen atractivo: conocimiento, oratoria, dialéctica, mística profesional, entre muchas que se pudieran citar. La mayoría de las veces simplemente nos quedamos en la función técnica del rol (ya que también es la que suele disfrutarse más) pero no por ello se pierden las otras. Pero lo importante en la arquitectura de software, es que el front-end que presentemos a los clientes y nuestra habilidad de transformar sus requerimientos en diseños que nuestros equipos puedan emplear efectivamente cambie acorde a los tiempos y las necesidades. Por ejemplo: un sistema operativo ofrece la mayoría de las veces más de un tipo de UI, ya sea gráfico, de consola o híbridos. Con cada necesidad,  y muchas veces gustos, se cambia el front-end aunque el back-end siga siendo el mismo.

Todo front-end, sin importar que tan perfecto y ergonómico esté diseñado, encontrará un individuo (o grupos de individuos) que no sabrán encontrar la forma de interactuar con él, por más que el mismo front-end se adapte. Este tipo de gorilas escudan su inaptitud a través de lo que al final llamaríamos política. Muchas veces se perderán proyectos increíbles y satisfactorios por las políticas internas de la corporación que formarán los obstáculos que a veces, contrario a lo que parezca, terminará con el alivio de la cancelación del proyecto. Pero un arquitecto no se decepciona de la política, sino que aprende de ella y de los restos que quedan del proyecto.

Ciertamente el arquitecto no es el único front-end que tiene una organización para con sus clientes, así como el equipo de desarrollo no es el único back-end. Existen otros actores que estoy seguro tienen en sus propios representantes, con sus respectivos blogs, y que serán muy interesantes de leer, pero en lo que a su servidor compete, este tratará de mis aventuras en el mundo de la arquitectura y una que otra cosa que creo les pueda gustar.

Espero les guste esta introducción y los futuros posts.