Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Programación orientada a aspectos con la clase RealProxy
Una aplicación con arquitectura sólida tiene capas separadas, para que las diferentes preocupaciones no interactúen más de lo necesario. Imagine que está diseñando una aplicación de acoplamiento flexible y fácil de mantener, pero a mitad del desarrollo nota que algunos de los requisitos tal vez no se ajusten bien a la arquitectura, como:
- La aplicación debe tener un sistema de autenticación, que se usa antes de cualquier consulta o actualización.
- Se deben validar los datos antes de escribirlos en la base de datos.
- La aplicación debe tener auditoría y registro para las operaciones razonables.
- La aplicación debe mantener un registro de depuración para comprobar si las operaciones están bien.
- Se debe medir el rendimiento de algunas operaciones para ver si están en el intervalo deseado.
Cualquiera de estos requisitos necesita mucho trabajo y, más que eso, la duplicación de código. Tiene que agregar el mismo código en varias partes del sistema, esto va contra el principio “No te repitas” (DRY, por sus siglas en inglés) y dificulta el mantenimiento. Cualquier modificación a los requisitos provoca un cambio masivo en el programa. Cuando agrego algo así a mis aplicaciones, pienso: "¿por qué no puedo usar el compilador para agregar este código repetido en varios lugares?" o "me gustaría contar con la opción de "agregar registro a este método"".
Las buenas noticias son que sí existe algo así: la programación orientada a aspectos (AOP). Establece una separación entre el código general y los aspectos que cruzan los límites de un objeto o capa. Por ejemplo, el registro de la aplicación no está vinculado a ninguna capa de aplicación. Se aplica a todo el programa y debería estar en todos lados. Eso se denomina una preocupación transversal.
AOP es, según Wikipedia, "un paradigma de programación cuya intención es permitir una adecuada modularización de las aplicaciones al permitir la separación de las inquietudes transversales". Se ocupa de la funcionalidad que ocurre en varias partes del sistema y la separa del núcleo de la aplicación, de esta forma se mejora la separación de inquietudes transversales a la vez que se evita la duplicación del código y el acoplamiento.
En este artículo explicaré los conceptos básicos de AOP y luego proporcionaré detalles sobre cómo simplificarlo mediante el uso de proxy dinámico a través de la clase RealProxy de Microsoft .NET Framework.
implementación de AOP
La ventaja más importante de AOP es que solo tiene que preocuparse del aspecto en un lugar, programarlo una vez y aplicarlo en todos los lugares que lo necesiten. AOP tiene muchos usos, como:
- La implementación de registros en su aplicación.
- Uso de la autenticación antes de realizar una operación (como el hecho de permitir ciertas operaciones solo para los usuarios autenticados).
- La implementación de validación o notificación para los establecedores de propiedades (llamadas al evento PropertyChanged cuando se modifique una propiedad para las clases que implementan la interfaz INotifyPropertyChanged).
- La modificación del comportamiento de algunos métodos.
Como puede ver, AOP tiene varios usos pero debe ejercerla con cuidado. Ocultará algunos códigos pero están ahí, ejecutándose en cada llamada en la que el aspecto esté presente. Puede contener problemas y tener un impacto considerable en el rendimiento de la aplicación. Un pequeño error en el aspecto podría significar horas de depuración. Si su aspecto no se usa en muchos lugares, a veces es mejor agregarlo directamente al código.
Las implementaciones de AOP usan algunas técnicas comunes:
- Adición de un código fuente mediante un preprocesador, como el que se usa en C++.
- Uso de un postprocesador para agregar instrucciones al código binario compilado.
- Uso de un compilador especial que agrega el código mientras realiza la compilación.
- Uso de un interceptor de código en tiempo de ejecución que intercepta la ejecución y agrega el código deseado.
En .NET Framework, la técnica más común es el uso de un postprocesador y de la intercepción de código. La primera es la técnica que usa PostSharp (postsharp.net) y la última es la que usan los contenedores de inserción de dependencia (DI), como Castle DynamicProxy (bit.ly/JzE631) y Unity (unity.codeplex.com). Por lo general, estas herramientas usan un patrón de diseño denominado Decorator o Proxy para interceptar el código.
El patrón de diseño Decorator
El patrón de diseño Decorator soluciona un problema común: tiene una clase y quiere agregarle algo de funcionalidad. Cuenta con varias opciones para lograrlo:
- Puede agregar la funcionalidad nueva directamente a la clase. Sin embargo, eso le otorga otra responsabilidad a la clase y afecta negativamente al principio de "responsabilidad única".
- Puede crear una clase nueva que ejecute esta funcionalidad y que la llame desde la clase antigua. Esto hace surgir un nuevo problema: ¿qué ocurre si quiere usar la clase sin la funcionalidad nueva?
- Podría heredar una clase nueva y agregar la nueva funcionalidad, pero eso podría dar como resultado varias clases nuevas. Por ejemplo, digamos que tiene una clase de repositorio para crear, leer, actualizar y eliminar (CRUD) operaciones de la base de datos y quiere agregarle auditoría. Más adelante, quiere agregar la validación de datos para asegurarse de que estos se usan de la forma correcta. Después, es posible que desee autenticar el acceso para asegurarse de que solo los usuarios autorizados puedan acceder a las clases. Estos son los problemas que podría encontrar: es posible que tenga clases que implementen los tres aspectos y algunas que implementen solo dos de ellos, o incluso uno solo. ¿Con cuántas clases terminará?
- Puede "decorar" la clase con el aspecto y crear una clase nueva que use el aspecto y que luego llame al antiguo. De esa forma, si necesita un aspecto, lo decora una vez. En el caso de dos aspectos, lo decora dos veces y así sucesivamente. Supongamos que solicita un juguete (como todos somos "geeks", una Xbox o un smartphone serían una buena opción). Este juguete necesita un paquete para estar en el mostrador que lo mantenga protegido. Luego, pide que se le incluya un envoltorio de regalo, la segunda decoración, para embellecer la caja con cintas, tarjetas y papel de regalo. La tienda envía el juguete en un tercer paquete, una caja con perlas de poliestireno para protegerlo. Tiene tres decoraciones, cada una tiene una funcionalidad diferente y cada una es independiente de la otra. Puede comprar el juguete sin envoltorio, ir a buscarlo a la tienda sin la caja externa o incluso comprarlo sin caja (y a un precio con descuento). Puede incluir cualquier combinación de decoraciones en su juguete, pero estas no afectarán sus funciones básicas.
Después de esta información sobre el patrón Decorator quiero mostrarle cómo implementarlo en C#.
Primero, cree una interfaz IRepository<T>:
public interface IRepository<T>
{
void Add(T entity);
void Delete(T entity);
void Update(T entity);
IEnumerable<T> GetAll();
T GetById(int id);
}
Impleméntela con la clase Repository<T>, que se muestra en la Figura 1.
Figura 1 La clase Repository<T>
public class Repository<T> : IRepository<T>
{
public void Add(T entity)
{
Console.WriteLine("Adding {0}", entity);
}
public void Delete(T entity)
{
Console.WriteLine("Deleting {0}", entity);
}
public void Update(T entity)
{
Console.WriteLine("Updating {0}", entity);
}
public IEnumerable<T> GetAll()
{
Console.WriteLine("Getting entities");
return null;
}
public T GetById(int id)
{
Console.WriteLine("Getting entity {0}", id);
return default(T);
}
}
Use la clase Repository<T> para agregar, actualizar, eliminar y recuperar los elementos de la clase Customer:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
}
Es posible que el programa tenga un aspecto similar al de la Figura 2.
Figura 2 El programa principal, sin inicio de sesión
static void Main(string[] args)
{
Console.WriteLine("***\r\n Begin program - no logging\r\n");
IRepository<Customer> customerRepository =
new Repository<Customer>();
var customer = new Customer
{
Id = 1,
Name = "Customer 1",
Address = "Address 1"
};
customerRepository.Add(customer);
customerRepository.Update(customer);
customerRepository.Delete(customer);
Console.WriteLine("\r\nEnd program - no logging\r\n***");
Console.ReadLine();
}
Cuando ejecute este código verá algo similar a lo que se ve en la Figura 3.
Figura 3 Resultado del programa sin inicio de sesión
Imagine que su jefe le pide agregar el registro a esta clase. Puede crear una clase nueva que decorará a IRepository<T>. Recibe la clase a crear e implementa la misma interfaz, como se aprecia en la Figura 4.
Figura 4 El repositorio del registrador
public class LoggerRepository<T> : IRepository<T>
{
private readonly IRepository<T> _decorated;
public LoggerRepository(IRepository<T> decorated)
{
_decorated = decorated;
}
private void Log(string msg, object arg = null)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(msg, arg);
Console.ResetColor();
}
public void Add(T entity)
{
Log("In decorator - Before Adding {0}", entity);
_decorated.Add(entity);
Log("In decorator - After Adding {0}", entity);
}
public void Delete(T entity)
{
Log("In decorator - Before Deleting {0}", entity);
_decorated.Delete(entity);
Log("In decorator - After Deleting {0}", entity);
}
public void Update(T entity)
{
Log("In decorator - Before Updating {0}", entity);
_decorated.Update(entity);
Log("In decorator - After Updating {0}", entity);
}
public IEnumerable<T> GetAll()
{
Log("In decorator - Before Getting Entities");
var result = _decorated.GetAll();
Log("In decorator - After Getting Entities");
return result;
}
public T GetById(int id)
{
Log("In decorator - Before Getting Entity {0}", id);
var result = _decorated.GetById(id);
Log("In decorator - After Getting Entity {0}", id);
return result;
}
}
Esta nueva clase ajusta los métodos para la clase decorada y agrega la característica de registro. Tiene que modificar un poco el código para llamar a la clase de registro, como se muestra en la Figura 5.
Figura 5 El programa principal con el repositorio del registrador
static void Main(string[] args)
{
Console.WriteLine("***\r\n Begin program - logging with decorator\r\n");
// IRepository<Customer> customerRepository =
// new Repository<Customer>();
IRepository<Customer> customerRepository =
new LoggerRepository<Customer>(new Repository<Customer>());
var customer = new Customer
{
Id = 1,
Name = "Customer 1",
Address = "Address 1"
};
customerRepository.Add(customer);
customerRepository.Update(customer);
customerRepository.Delete(customer);
Console.WriteLine("\r\nEnd program - logging with decorator\r\n***");
Console.ReadLine();
}
Lo único que debe hacer es crear la nueva clase, pasando una instancia de la clase antigua como parámetro para su constructor. Cuando ejecute el programa, podrá ver que tiene la característica de registro, como se aprecia en la Figura 6.
Figura 6 Ejecución del programa de registro con un decorador
Tal vez usted piense: "de acuerdo, la idea es bueno pero significa mucho trabajo: tengo que implementar todas las clases y agregar el aspecto a todos los métodos. Será algo difícil de mantener. ¿Hay otra forma de hacerlo?" Con .NET Framework, puede usar la reflexión para conseguir todos los métodos y ejecutarlos. La biblioteca de clase de base de (BCL) tiene incluso la clase RealProxy (bit.ly/18MfxWo), que realiza la implementación por usted.
Creación de un proxy dinámico con RealProxy
La clase RealProxy le da la funcionalidad básica para los proxies. Es una clase abstracta que se tiene que heredar mediante la anulación del método Invoke y con la adición de la nueva funcionalidad. Esta clase está en el espacio de nombres System.Runtime.Remoting.Proxies. Para crear un proxy dinámico puede usar un código parecido al de la Figura 7.
Figura 7 La clase de proxy dinámico
class DynamicProxy<T> : RealProxy
{
private readonly T _decorated;
public DynamicProxy(T decorated)
: base(typeof(T))
{
_decorated = decorated;
}
private void Log(string msg, object arg = null)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(msg, arg);
Console.ResetColor();
}
public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
var methodInfo = methodCall.MethodBase as MethodInfo;
Log("In Dynamic Proxy - Before executing '{0}'",
methodCall.MethodName);
try
{
var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
Log("In Dynamic Proxy - After executing '{0}' ",
methodCall.MethodName);
return new ReturnMessage(result, null, 0,
methodCall.LogicalCallContext, methodCall);
}
catch (Exception e)
{
Log(string.Format(
"In Dynamic Proxy- Exception {0} executing '{1}'", e),
methodCall.MethodName);
return new ReturnMessage(e, methodCall);
}
}
}
Cuando se encuentre en el constructor de la clase, tiene que llamar al constructor de la clase base y pasar el tipo de la clase que se va a decorar. Luego, tiene que anular el método Invoke que recibe el parámetro IMessage. Contiene un directorio que incluye todos los parámetros que se pasan para el método. El parámetro IMessage está encasillado en un IMethodCallMessage, así que puede extraer el parámetro MethodBase (que tiene el tipo MethodInfo).
Para agregar el aspecto que desea antes de llamar al método, llame al método original con methodInfo.Invoke y agregue el aspecto después de la llamada.
No puede llamar a su proxy directamente, ya que DynamicProxy<T> no es un IRepository<Customer>. Eso significa que no puede hacer lo siguiente para llamarlo:
IRepository<Customer> customerRepository =
new DynamicProxy<IRepository<Customer>>(
new Repository<Customer>());
Para usar el repositorio decorado, debe usar el método GetTransparentProxy, que regresará una instancia de IRepository<Customer>. Cada método de la instancia que se llama pasará a través del método Invoke de proxy. Para facilitar el proceso puede crear una clase Factory, que le ayudará a crear el proxy y a regresar la instancia para el repositorio:
public class RepositoryFactory
{
public static IRepository<T> Create<T>()
{
var repository = new Repository<T>();
var dynamicProxy = new DynamicProxy<IRepository<T>>(repository);
return dynamicProxy.GetTransparentProxy() as IRepository<T>;
}
}
De esa forma, el programa principal será como el que se ve en la Figura 8.
Figura 8 El programa principal con un proxy dinámico
static void Main(string[] args)
{
Console.WriteLine("***\r\n Begin program - logging with dynamic proxy\r\n");
// IRepository<Customer> customerRepository =
// new Repository<Customer>();
// IRepository<Customer> customerRepository =
// new LoggerRepository<Customer>(new Repository<Customer>());
IRepository<Customer> customerRepository =
RepositoryFactory.Create<Customer>();
var customer = new Customer
{
Id = 1,
Name = "Customer 1",
Address = "Address 1"
;
customerRepository.Add(customer);
customerRepository.Update(customer);
customerRepository.Delete(customer);
Console.WriteLine("\r\nEnd program - logging with dynamic proxy\r\n***");
Console.ReadLine();
}
Cuando ejecute este programa obtendrá un resultado parecido al anterior, como puede ver en la Figura 9.
Figura 9 Ejecución de programas con un proxy dinámico
Como puede ver, ha creado un proxy dinámico que permite agregar aspectos al código, sin la necesidad de repetición. Si desea agregar un aspecto nuevo, solo tiene que crear un nueva clase, heredar de RealProxy y usarla para decorar el primer proxy.
Si su jefe regresa y le pide agregar la característica de autorización al código, para que los únicos que puedan acceder al código sean los administradores, podría crear un nuevo proxy, como se ve en la Figura 10.
Figura 10 Proxy de autenticación
class AuthenticationProxy<T> : RealProxy
{
private readonly T _decorated;
public AuthenticationProxy(T decorated)
: base(typeof(T))
{
_decorated = decorated;
}
private void Log(string msg, object arg = null)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(msg, arg);
Console.ResetColor();
}
public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
var methodInfo = methodCall.MethodBase as MethodInfo;
if (Thread.CurrentPrincipal.IsInRole("ADMIN"))
{
try
{
Log("User authenticated - You can execute '{0}' ",
methodCall.MethodName);
var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
return new ReturnMessage(result, null, 0,
methodCall.LogicalCallContext, methodCall);
}
catch (Exception e)
{
Log(string.Format(
"User authenticated - Exception {0} executing '{1}'", e),
methodCall.MethodName);
return new ReturnMessage(e, methodCall);
}
}
Log("User not authenticated - You can't execute '{0}' ",
methodCall.MethodName);
return new ReturnMessage(null, null, 0,
methodCall.LogicalCallContext, methodCall);
}
}
Hay que modificar al generador del repositorio para que llame a ambos proxies, como se ve en la Figura 11.
Figura 11 El generador del repositorio decorado con dos proxies
public class RepositoryFactory
{
public static IRepository<T> Create<T>()
{
var repository = new Repository<T>();
var decoratedRepository =
(IRepository<T>)new DynamicProxy<IRepository<T>>(
repository).GetTransparentProxy();
// Create a dynamic proxy for the class already decorated
decoratedRepository =
(IRepository<T>)new AuthenticationProxy<IRepository<T>>(
decoratedRepository).GetTransparentProxy();
return decoratedRepository;
}
}
Cuando cambia el programa principal por uno como el de la Figura 12 y lo ejecuta, tendrá un resultado como el de la Figura 13.
Figura 12 El programa principal llamando al repositorio con dos usuarios
static void Main(string[] args)
{
Console.WriteLine(
"***\r\n Begin program - logging and authentication\r\n");
Console.WriteLine("\r\nRunning as admin");
Thread.CurrentPrincipal =
new GenericPrincipal(new GenericIdentity("Administrator"),
new[] { "ADMIN" });
IRepository<Customer> customerRepository =
RepositoryFactory.Create<Customer>();
var customer = new Customer
{
Id = 1,
Name = "Customer 1",
Address = "Address 1"
};
customerRepository.Add(customer);
customerRepository.Update(customer);
customerRepository.Delete(customer);
Console.WriteLine("\r\nRunning as user");
Thread.CurrentPrincipal =
new GenericPrincipal(new GenericIdentity("NormalUser"),
new string[] { });
customerRepository.Add(customer);
customerRepository.Update(customer);
customerRepository.Delete(customer);
Console.WriteLine(
"\r\nEnd program - logging and authentication\r\n***");
Console.ReadLine();
}
Figura 13 Resultado del programa cuando usa dos proxies
El programa ejecuta dos veces los métodos del repositorio. La primera vez, se ejecuta como usuario administrador y se llama a los métodos. La segunda vez, se ejecuta como usuario normal y se omite a los métodos.
Es mucho más sencillo, ¿no es así? Observe que el generador regresa una instancia de IRepository<T>, para que el programa no se de cuenta cuando use la versión decorada. Esto respeta el Principio de sustitución de Liskov, que señala que si S es un subtipo de T, entonces los objetos de tipo T se pueden reemplazar con objetos de tipo S. En este caso, si usa la interfaz IRepository<Customer>, podría usar cualquier clase que implemente esta interfaz, sin la necesidad de modificar el programa.
Funciones de filtrado
Hasta ahora, las funciones no contaban con filtrado; se aplica el aspecto a cada método de clase que se llame. Este no suele ser el comportamiento esperado. Por ejemplo, tal vez no quiera registrar los métodos de recuperación (GetAll y GetById). Una forma de lograr esto es filtrar el aspecto por nombre, como muestra la Figura 14.
Figura 14 Métodos de filtrado para el aspecto
public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
var methodInfo = methodCall.MethodBase as MethodInfo;
if (!methodInfo.Name.StartsWith("Get"))
Log("In Dynamic Proxy - Before executing '{0}'",
methodCall.MethodName);
try
{
var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
if (!methodInfo.Name.StartsWith("Get"))
Log("In Dynamic Proxy - After executing '{0}' ",
methodCall.MethodName);
return new ReturnMessage(result, null, 0,
methodCall.LogicalCallContext, methodCall);
}
catch (Exception e)
{
if (!methodInfo.Name.StartsWith("Get"))
Log(string.Format(
"In Dynamic Proxy- Exception {0} executing '{1}'", e),
methodCall.MethodName);
return new ReturnMessage(e, methodCall);
}
}
El programa comprueba si el método comienza con "Get". Si es así, no aplica el aspecto. Esto sirve, pero el código de filtrado se repite tres veces. Además de eso, el filtro se encuentra dentro del proxy, lo que le hará cambiar la clase cada vez que desee cambiar el proxy. Puede mejorar esta situación con la creación de un predicado IsValidMethod:
private static bool IsValidMethod(MethodInfo methodInfo)
{
return !methodInfo.Name.StartsWith("Get");
}
Ahora tiene que realizar el cambio en un solo lugar, pero de todas formas tendrá que cambiar la clase cada vez que desee cambiar el filtro. Una solución sería exponer el filtro como una propiedad de clase, para que pueda asignarle al autor de la llamada la responsabilidad de crear un filtro. Puede crear una propiedad Filter de tipo Predicate<MethodInfo> y usarla para filtrar los datos, como se ve en la Figura 15.
Figura 15 Un proxy de filtrado
class DynamicProxy<T> : RealProxy
{
private readonly T _decorated;
private Predicate<MethodInfo> _filter;
public DynamicProxy(T decorated)
: base(typeof(T))
{
_decorated = decorated;
_filter = m => true;
}
public Predicate<MethodInfo> Filter
{
get { return _filter; }
set
{
if (value == null)
_filter = m => true;
else
_filter = value;
}
}
private void Log(string msg, object arg = null)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(msg, arg);
Console.ResetColor();
}
public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
var methodInfo = methodCall.MethodBase as MethodInfo;
if (_filter(methodInfo))
Log("In Dynamic Proxy - Before executing '{0}'",
methodCall.MethodName);
try
{
var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
if (_filter(methodInfo))
Log("In Dynamic Proxy - After executing '{0}' ",
methodCall.MethodName);
return new ReturnMessage(result, null, 0,
methodCall.LogicalCallContext, methodCall);
}
catch (Exception e)
{
if (_filter(methodInfo))
Log(string.Format(
"In Dynamic Proxy- Exception {0} executing '{1}'", e),
methodCall.MethodName);
return new ReturnMessage(e, methodCall);
}
}
}
La propiedad Filter se inicia con Filter = m => true. Eso significa que no hay filtros activos. Cuando se asigna la propiedad Filter, el programa comprueba si el valor es nulo y, de ser así, restablece el filtro. En la ejecución del método Invoke, el programa comprueba el resultado del filtro y aplica el aspecto solo si es verdadero. Ahora la creación de proxy en la clase de generador tiene este aspecto:
public class RepositoryFactory
{
public static IRepository<T> Create<T>()
{
var repository = new Repository<T>();
var dynamicProxy = new DynamicProxy<IRepository<T>>(repository)
{
Filter = m => !m.Name.StartsWith("Get")
};
return dynamicProxy.GetTransparentProxy() as IRepository<T>;
}
}
}
La responsabilidad de crear el filtro se transfirió al generador. Cuando ejecute el programa, debería obtener algo similar a lo que se ve en la Figura 16.
Figura 16 Resultado con un proxy filtrado
Observe que en la Figura 16 los dos últimos métodos, GetAll y GetById (que se representan con “Getting entities” y “Getting entity 1”), no cuentan con el registro. Puede mejorar aún más la clase si expone los aspectos en forma de eventos. De esa forma, no tiene que cambiar la clase cada vez que desee cambiar el aspecto. Esto se muestra en la Figura 17.
Figura 17 Un proxy flexible
class DynamicProxy<T> : RealProxy
{
private readonly T _decorated;
private Predicate<MethodInfo> _filter;
public event EventHandler<IMethodCallMessage> BeforeExecute;
public event EventHandler<IMethodCallMessage> AfterExecute;
public event EventHandler<IMethodCallMessage> ErrorExecuting;
public DynamicProxy(T decorated)
: base(typeof(T))
{
_decorated = decorated;
Filter = m => true;
}
public Predicate<MethodInfo> Filter
{
get { return _filter; }
set
{
if (value == null)
_filter = m => true;
else
_filter = value;
}
}
private void OnBeforeExecute(IMethodCallMessage methodCall)
{
if (BeforeExecute != null)
{
var methodInfo = methodCall.MethodBase as MethodInfo;
if (_filter(methodInfo))
BeforeExecute(this, methodCall);
}
}
private void OnAfterExecute(IMethodCallMessage methodCall)
{
if (AfterExecute != null)
{
var methodInfo = methodCall.MethodBase as MethodInfo;
if (_filter(methodInfo))
AfterExecute(this, methodCall);
}
}
private void OnErrorExecuting(IMethodCallMessage methodCall)
{
if (ErrorExecuting != null)
{
var methodInfo = methodCall.MethodBase as MethodInfo;
if (_filter(methodInfo))
ErrorExecuting(this, methodCall);
}
}
public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
var methodInfo = methodCall.MethodBase as MethodInfo;
OnBeforeExecute(methodCall);
try
{
var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
OnAfterExecute(methodCall);
return new ReturnMessage(
result, null, 0, methodCall.LogicalCallContext, methodCall);
}
catch (Exception e)
{
OnErrorExecuting(methodCall);
return new ReturnMessage(e, methodCall);
}
}
}
En la Figura 17, los métodos OnBeforeExecute, OnAfterExecute y OnErrorExecuting llaman a tres eventos (BeforeExecute, AfterExecute y ErrorExecuting). Estos métodos comprueben si el controlador de eventos está definido, de ser así, comprueban si el método que se llamó pasa el filtro. Si es así, llaman al controlador de eventos que aplica el aspecto. La clase del generador se convierte en algo similar a lo que se ve en la Figura 18.
Figura 18 Un generador del repositorio que establece el aspecto de los eventos y el filtro
public class RepositoryFactory
{
private static void Log(string msg, object arg = null)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(msg, arg);
Console.ResetColor();
}
public static IRepository<T> Create<T>()
{
var repository = new Repository<T>();
var dynamicProxy = new DynamicProxy<IRepository<T>>(repository);
dynamicProxy.BeforeExecute += (s, e) => Log(
"Before executing '{0}'", e.MethodName);
dynamicProxy.AfterExecute += (s, e) => Log(
"After executing '{0}'", e.MethodName);
dynamicProxy.ErrorExecuting += (s, e) => Log(
"Error executing '{0}'", e.MethodName);
dynamicProxy.Filter = m => !m.Name.StartsWith("Get");
return dynamicProxy.GetTransparentProxy() as IRepository<T>;
}
}
Ahora tiene una clase proxy flexible y puede seleccionar los aspectos que se aplicarán solo a los métodos seleccionados antes de la ejecución, después de la misma o cuando haya un error. Teniendo esto en cuenta, puede aplicar varios aspectos en su código sin necesidad de repetición y de una manera sencilla.
No es un reemplazo
Con AOP puede agregar código a todas las capas de su aplicación de una forma centralizada, sin la necesidad de repetir el código. Demostré cómo crear un proxy dinámico genérico basado en el patrón de diseño Decorator, que aplica aspectos a sus clases con eventos y un predicado para filtrar las funciones que desee.
Como puede ver, la clase RealProxy es una clase flexible que le proporciona un control total del código, sin dependencias externas. Sin embargo, tenga en cuenta que RealProxy no es un reemplazo para las otras herramientas de AOP, como PostSharp. PostSharp usa un método completamente diferente. Agregará un código de lenguaje intermedio (IL) en un paso posterior a la compilación y no usará la reflexión, por lo que debería tener un rendimiento superior a RealProxy. Además, requiere más trabajo implementar un aspecto con RealProxy que con PostSharp. Con PostSharp, solo tiene que crear la clase del aspecto y agregarle un atributo (o método) donde desee agregar el aspecto, eso es todo.
Por otro lado, con RealProxy tendrá el control total del código fuente, sin dependencias externas, y podrá ampliarlo y personalizarlo tanto como desee. Por ejemplo, si quiere aplicar un aspecto únicamente en los métodos que cuentan con el atributo Log, podría hacer lo siguiente:
public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
var methodInfo = methodCall.MethodBase as MethodInfo;
if (!methodInfo.CustomAttributes
.Any(a => a.AttributeType == typeof (LogAttribute)))
{
var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
return new ReturnMessage(result, null, 0,
methodCall.LogicalCallContext, methodCall);
}
...
Además de eso, la técnica que usa RealProxy (interceptar el código y permitir que el programe lo reemplace) es muy eficaz. Por ejemplo, si desea crear un marco ficticio para crear simulacros y auxiliares de prueba, puede usar la clase RealProxy para interceptar todas las llamadas y reemplazarlas con su propio comportamiento, aunque eso es tema para otro artículo.
Bruno Sonnino trabaja en Brasil como uno de los Profesionales más valiosos de Microsoft (MVP). Es desarrollador, consultor y autor de cinco libros de Delphi que Pearson Education Brazil publicó en portugués, además de varios artículos para revistas de Brasil y de Estados Unidos y sitios Web.
Gracias a los siguientes expertos técnicos de Microsoft Research por su ayuda en la revisión de este artículo: James McCaffrey, Carlos Suarez y Johan Verwey
James McCaffrey trabaja para Microsoft Research en Redmond, Wash. Ha colaborado en el desarrollo de varios productos de Microsoft como, por ejemplo, Internet Explorer y Bing. Puede ponerse en contacto con él en jammc@microsoft.com.
Carlos García Jurado Suarez es ingeniero de software de investigación en Microsoft Research, donde trabajó en el equipo de desarrollo avanzado y, más recientemente, en Machine Learning Group. Antes de eso, fue desarrollador en Visual Studio, donde trabajó en herramientas de modelado como el Diseñador de clases.