Programação orientada a aspectos

Programação orientada a aspectos com a classe RealProxy

Bruno Sonnino

Um aplicativo bem-arquitetado tem camadas separadas para que preocupações diferentes não interajam mais do que o necessário. Imagine que você está desenhando um aplicativo flexível e de fácil manutenção, mas no meio do desenvolvimento, percebe que alguns requisitos podem não se encaixar bem na arquitetura, como:

  • O aplicativo deve ter um sistema de autenticação, usado antes de qualquer consulta ou atualização.
  • Os dados devem ser validados antes de serem gravados no banco de dados.
  • O aplicativo deve ter auditoria e registro em log para as operações delicadas.
  • O aplicativo deve manter um log de depuração para verificar se as operações estão OK.
  • Algumas operações devem ter seu desempenho mensurado para ver se estão na faixa desejada.

Qualquer um desses requisitos precisa de muito trabalho e, mais que isso, a duplicação de código. Você tem de adicionar o mesmo código em muitas partes do sistema, o que vai contra o princípio de DRY (“não se repetir”) e torna a manutenção mais difícil. Qualquer mudança de requisito provoca uma grande mudança no programa. Quando tenho de adicionar algo como isso em meus aplicativos, penso: "Por que o compilador não pode adicionar esse código repetido em vários lugares para mim?" ou "Eu queria ter alguma opção para adicionar registro em log a este método".

A boa notícia é que algo assim existe: a programação orientada a aspecto (AOP). Ela separa o código geral dos aspectos que cruzam as fronteiras de um objeto ou de uma camada. Por exemplo, o log do aplicativo não está vinculado a qualquer camada de aplicativo. Isso se aplica a todo o programa e deve estar presente em todos os lugares. Isso chama-se preocupação transversal.

AOP é, de acordo com a Wikipedia, "um paradigma de programação que tem como objetivo aumentar a modularidade, permitindo a separação de preocupações transversais". Trata-se de uma funcionalidade que ocorre em várias partes do sistema e a separa do núcleo do aplicativo, melhorando, assim, a separação de preocupações, evitando duplicação de código e acoplamento.

Neste artigo, vou explicar os conceitos básicos do AOP e detalhar como torná-lo mais fácil usando um proxy dinâmico com a classe RealProxy do Microsoft .NET Framework.

Implementando o AOP

A maior vantagem do AOP é que você só precisa se preocupar com o aspecto em um só lugar, programá-lo uma vez e aplicá-lo em todos os lugares em que for necessário. Há muitos usos para o AOP, como:

  • Implementação de registro em log em seu aplicativo.
  • Uso da autenticação antes de uma operação (como permitir algumas operações apenas para usuários autenticados).
  • Implementação de validação ou notificação para setters de propriedade (chamando o evento PropertyChanged quando uma propriedade é alterado para as classes que implementam a interface INotifyPropertyChanged).
  • Alteração do comportamento de alguns métodos.

Como pode ver, o AOP tem muitos usos, mas você deve exercê-lo com cuidado. Isso manterá parte do código fora de sua vista, mas ele ainda está lá, executando em todas as chamadas onde o aspecto está presente. Ele pode ter bugs e afetar gravemente o desempenho do aplicativo. Um bug sutil no aspecto pode custar-lhe muitas horas de depuração. Se seu aspecto não for usado em muitos lugares, às vezes é melhor adicioná-lo diretamente ao código.

As implementações de AOP usam algumas técnicas comuns:

  • Adição de código-fonte usando um pré-processador, como o do C++.
  • Uso de um pós-processador para adicionar instruções no código binário compilado.
  • Uso de um compilador especial que adiciona o código durante a compilação.
  • Uso de um interceptor de código no tempo de execução que intercepta a execução e adiciona o código desejado.

No .NET Framework, a mais comumente usada destas técnicas são pós-processamento e interceptação de código. A primeira é a técnica usada pelo PostSharp (postsharp.net) e a última é usada por contêineres de injeção de dependência (DI), como Castle DynamicProxy (bit.ly/JzE631) e Unity (unity.codeplex.com). Essas ferramentas geralmente usam um padrão de design chamado Decorador ou Proxy para executar a interceptação de código.

O padrão de design Decorador

O padrão de design Decorador resolve um problema comum: Você tem uma classe e quero adicionar funcionalidade a ele. Você tem várias opções para isso:

  • Você pode adicionar a nova funcionalidade à classe diretamente. No entanto, isso dá à classe uma outra responsabilidade e fere o princípio da "responsabilidade única".
  • Você poderia criar uma nova classe que execute essa funcionalidade e a chame a partir da antiga classe. Isso traz um novo problema: E se você também quiser usar a classe sem a nova funcionalidade?
  • Você poderia herdar uma nova classe e adicionar a nova funcionalidade, mas que pode resultar em muitas novas classes. Por exemplo, digamos que você tenha uma classe de repositório para operações CRUD (Create, Read, Update, Delete) de banco de dados e queira adicionar auditoria. Mais tarde, você quer adicionar a validação de dados para verificar se os dados estão sendo atualizados corretamente. Depois disso, você também pode querer autenticar o acesso para garantir que somente usuários autorizados possam acessar as classes. Estes são os grandes problemas: Você poderia ter algumas classes que implementem os três aspectos e algumas que implementem apenas dois deles ou até mesmo apenas um. Quantas classes você acabaria tendo?
  • Você pode "decorar" a classe com o aspecto, criando de uma nova classe que usa o aspecto e depois chama a antiga. Dessa forma, se um aspecto for necessário, você o decorará uma vez. Para dois aspectos, você o decora duas vezes e assim por diante. Digamos que você encomende um brinquedo (já que todos nós somos nerds, um Xbox ou um smartphone está OK). Ela precisa de um pacote para exibir na loja e para proteção. Então, você o encomenda com embrulho para presente, a segunda decoração, para embelezar a caixa com fitas, faixas, cartões e papel de presente. A loja envia o brinquedo com um terceiro pacote, uma caixa com bolas de isopor para proteção. Você tem três decorações, cada uma com uma funcionalidade diferente, e cada uma independente uma da outra. Você pode comprar seu brinquedo sem embalagem para presente, buscá-lo na loja sem a caixa externa ou até mesmo comprá-lo sem caixa (com um desconto especial!). Você pode ter seu brinquedo com qualquer combinação das decorações, mas elas não mudam sua funcionalidade básica.

Agora que você conhece o padrão Decorador, vou mostrar como implementá-lo em C#.

Primeiro, crie uma interface 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);
}

Implemente-o com a classe Repository<T>, mostrada na Figura 1.

Figura 1 A classe 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 a classe Repository<T> para adicionar, atualizar, excluir e recuperar os elementos da classe Customer:

public class Customer
{
  public int Id { get; set; }
  public string Name { get; set; }
  public string Address { get; set; }
}

O programa poderia ficar semelhante ao da Figura 2.

Figura 2 O programa principal, sem registro em log

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();
}

Quando você executar este código, verá algo parecido com a Figura 3.

Output of the Program with No Logging
Figura 3 Resultado do programa sem registro em log

Imagine que seu chefe lhe pede para adicionar um registro em log a essa classe. Você pode criar uma nova classe que decorará IRepository<T>. Ela recebe a classe de compilar e implementa a mesma interface, como mostrado na Figura 4.

Figura 4 O repositório do Agente

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 nova classe encapsula os métodos para a classe decorada e adiciona o recurso de registro em log. Você deve alterar o código um pouco para chamar a classe de log, como mostra a Figura 5.

Figura 5 O programa principal usando o repositório do Agente

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();
}

Basta criar a nova classe, passando uma instância da classe antiga como parâmetro para seu construtor. Ao executar o programa, você pode ver que ele tem o registro em log, como mostra a Figura 6.

Execution of the Logging Program with a Decorator
Figura 6 Execução do programa de registro em log com um Decorador

Você deve estar pensando: “Tudo bem, a ideia é boa, mas dá muito trabalho: tenho de implementar todas as classes e adicionar o aspecto a todos os métodos. Isso será difícil de manter. Há outra maneira de fazê-lo? "Com o .NET Framework, você pode usar a reflexão para obter todos os métodos e executá-los. A biblioteca de classes base (BCL) tem até a classe RealProxy (bit.ly/18MfxWo) que faz a implementação para você.

Criando um proxy dinâmico com o RealProxy

A classe RealProxy oferece funcionalidade básica para proxies. É uma classe abstrata que deve ser herdada, substituindo seu método Invoke e adicionando novas funcionalidades. Esta classe está no namespace System.Runtime.Remoting.Proxies. Para criar um proxy dinâmico, você pode usar um código semelhante à Figura 7.

Figura 7 A classe 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);
    }
  }
}

No construtor da classe, você deve chamar o construtor da classe base, passando o tipo da classe a ser decorada. Em seguida, você deve substituir o método Invoke que recebe um parâmetro IMessage. Ele contém um dicionário com todos os parâmetros passados para o método. O parâmetro IMessage é estereotipado para um IMethodCallMessage, então, você pode extrair o parâmetro MethodBase (que tem o tipo MethodInfo).

As próximas etapas são adicionar o aspecto que você deseja antes de chamar o método, chamar o método original com methodInfo.Invoke e adicionar o aspecto após a chamada.

Você não pode chamar seu proxy diretamente, porque DynamicProxy <T> não é IRepository<Customer>. Isso significa que você não pode chamá-lo dessa forma:

IRepository<Customer> customerRepository =
  new DynamicProxy<IRepository<Customer>>(
  new Repository<Customer>());

Para usar o repositório decorado, você deve usar o método GetTransparentProxy, que retornará uma instância de IRepository<Customer>. Cada método dessa instância que for chamado passará pelo método Invoke do proxy. Para facilitar esse processo, você pode criar uma classe Factory para criar o proxy e retornar a instância para o repositório:

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>;
  }
}

Dessa forma, o programa principal será semelhante ao da Figura 8.

Figura 8 O programa principal com um 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();
}

Ao executar este programa, você obtém um resultado similar ao de antes, como mostra a Figura 9.

Program Execution with Dynamic Proxy
Figura 9 Execução do programa com proxy dinâmico

Como pode ver, você criou um proxy dinâmico, que permite adicionar aspectos ao código, sem a necessidade de repeti-lo. Se você quisesse adicionar um novo aspecto, precisaria apenas criar uma nova classe, herdar de RealProxy e usá-la para decorar o primeiro proxy.

Se seu chefe for até você novamente e lhe pedir para adicionar autorização ao código, somente os administradores poderão acessar o repositório; você poderia criar um novo proxy, como mostra a Figura 10.

Figura 10 Um proxy de autenticação

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);
  }
}

A fábrica de repositórios deve ser alterada para chamar os dois proxies, como mostra a Figura 11.

Figura 11 A fábrica de repositórios decorada por dois 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;
  }
}

Ao alterar o programa principal para a Figura 12 e executá-lo, você obterá o resultado mostrado na Figura 13.

Figura 12 O programa principal chamando o repositório com dois usuários

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();
}

Output of the Program Using Two Proxies
Figura 13 Resultado do programa usando dois proxies

O programa executa os métodos de repositório duas vezes. Na primeira vez, ele é executado como usuário administrador e os métodos são chamados. Na segunda vez, ele é executado como usuário normal e os métodos são ignorados.

Bem mais fácil, não é? Observe que a fábrica retorna uma instância de IRepository<T>, então, o programa não sabe se ele está usando a versão decorada. Esta medida respeita o princípio da substituição de Liskov, que diz que, se S for um subtipo de T, os objetos do tipo T podem ser substituídos por objetos do tipo S. Neste caso, usando uma interface IRepository <Customer>, você pode usar qualquer classe que implemente essa interface, sem alteração no programa.

Funções de filtragem

Até agora, não houve filtragem nas funções, o aspecto é aplicado a cada método de classe que é chamado. Muitas vezes, esse não é o comportamento desejado. Por exemplo, você pode não querer registrar em log os métodos de recuperação (GetAll e GetById). Uma maneira de conseguir isso é filtrar o aspecto por nome, como na Figura 14.

Figura 14 Métodos de filtragem para o 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);
  }
}

O programa verifica se o método começa com "Get". Em caso positivo, o programa não aplica o aspectos. Isso funciona, mas o código de filtragem é repetido três vezes. Além disso, o filtro fica dentro do proxy, que fará com que você mude a classe toda vez que quiser alterar o proxy. Você pode melhorar isso criando um predicado IsValidMethod:

private static bool IsValidMethod(MethodInfo methodInfo)
{
  return !methodInfo.Name.StartsWith("Get");
}

Agora você precisa fazer a mudança em apenas um lugar, mas ainda precisa alterar a classe toda vez que quiser alterar o filtro. Uma solução para isso seria expor o filtro como uma propriedade da classe, para que possa atribuir a responsabilidade de criar um filtro para o chamador. Você pode criar uma propriedade Filter do tipo Predicate<MethodInfo> e usá-lo para filtrar os dados, como mostra a Figura 15.

Figura 15 Um proxy de filtragem

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);
    }
  }
}

A propriedade Filter é inicializada com Filter = m = true>. Isso significa que não há filtro ativo. Ao atribuir a propriedade Filter, o programa verifica se o valor é nulo e, em caso positivo, redefine o filtro. Na execução do método Invoke, o programa verifica o resultado do filtro e, se true, aplica o aspecto. Agora, a criação do proxy na classe de fábrica é semelhante a esta:

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>;
    }  
  }
}

A responsabilidade da criação do filtro foi transferida para a fábrica. Quando você executar o programa, deverá obter algo parecido com a Figura 16.

Output with a Filtered Proxy
Figura 16 Resultado com um proxy filtrado

Observe na Figura 16 que os dois últimos métodos, GetAll e GetById (representados por "Getting entidades" e "Getting entity 1") não têm o registro em log. Você pode melhorar a classe ainda mais, expondo os aspectos como eventos. Dessa forma, você não precisa alterar a classe toda vez que quiser alterar o aspecto. Isso é mostrado na Figura 17.

Figura 17 Um proxy flexível

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);
    }
  }
}

Na Figura 17, três eventos, BeforeExecute, AfterExecute e ErrorExecuting, são chamados pelos métodos OnBeforeExecute, OnAfterExecute e OnErrorExecuting. Esses métodos verificam se o manipulador de eventos está definido e, se estiver, eles verificam se o método chamado passa o filtro. Em caso positivo, eles chamam o manipulador de eventos que aplica o aspecto. A classe factory agora torna-se algo parecido com a Figura 18.

Figura 18 Uma fábrica de repositórios que define os eventos e filtros de aspecto

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>;
  }
}

Agora você tem uma classe de proxy flexível e pode escolher os aspectos que devem ser aplicados antes da execução, após a execução ou quando há um erro, e somente para os métodos selecionados. Com isso, você pode aplicar muitos aspectos em seu código sem repetição de forma simples.

Não é substituição

Com o AOP, você pode adicionar código a todas as camadas da seu aplicativo de forma centralizada, sem a necessidade de repetir o código. Mostrei como criar um proxy dinâmico genérico baseado no padrão de design Decorador que aplica os aspectos às suas classes usando eventos e um predicado para filtrar as funções que você deseja.

Como você pode ver, a classe RealProxy é uma classe flexível e lhe dá controle total do código, sem dependências externas. No entanto, observe que RealProxy não é um substituto para outras ferramentas do AOP, como PostSharp. O PostSharp usa um método completamente diferente. Ele adicionará código em linguagem intermediária (IL) em uma etapa pós-compilação e não usará a reflexão, por isso deve ter um desempenho melhor do que o RealProxy. Você também terá mais trabalho para implementar um aspecto com o RealProxy do que com o PostSharp. Com o PostSharp, você só precisa criar a classe de aspecto e adicionar um atributo à classe (ou ao método) onde deseja que o aspecto seja adicionado, e pronto.

Por outro lado, com o RealProxy, você terá controle total do seu código-fonte, sem dependências externas, e você pode estender e personalizar o quanto quiser. Por exemplo, se você quiser aplicar um aspecto apenas a métodos que possuem o atributo Log, poderá fazer algo parecido com isto:

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);
  }
    ...

Além disso, a técnica usada pelo RealProxy (interceptação de código e permitir que o programa o substitua) é poderosa. Por exemplo, se você quiser criar uma estrutura de simulação para criar simulações e stubs genéricos para o teste, você pode usar a classe RealProxy para interceptar todas as chamadas e substituí-las por seu próprio comportamento, mas isso é um assunto para outro artigo!

Bruno Sonnino é um Microsoft MVP (Most Valuable Professional) situado no Brasil. Ele é desenvolvedor, consultor e autor, que já escreveu cinco livros sobre Delphi, publicados em português pela Pearson Education do Brasil, e muitos artigos para revistas e sites do Brasil e dos Estados Unidos.

Agradecemos aos seguintes especialistas técnicos da Microsoft Research pela revisão deste artigo: James McCaffrey, Carlos Suarez e Johan Verwey
James McCaffrey trabalha para a Microsoft Research em Redmond, Washington. Ele trabalhou em vários produtos da Microsoft, incluindo Internet Explorer e Bing. Entre em contato com ele pelo email jammc@microsoft.com.

Carlos Garcia Jurado Suarez é um engenheiro de software de pesquisa da Microsoft Research, onde trabalha na equipe de desenvolvimento avançado e, mais recentemente, no grupo de aprendizagem por computador. No início, ele era desenvolvedor em Visual Studio que trabalhava com ferramentas de modelagem como o Designer de Classe.