Compartilhar via


Este artigo foi traduzido por máquina.

Cutting Edge

Programação orientada a aspectos, intercepção e o Unity 2.0

Dino Esposito

There’s no doubt that object orientation is a mainstream programming paradigm, one that excels when it comes to breaking a system down into components and describing processes through components. O paradigma da orientação a objeto (OO) também se destaca quando lidamos com questões de um componente específicas de negócios. No entanto, o paradigma da OO não é eficaz quando se trata de lidar com questões abrangentes. De uma forma geral, uma questão abrangente é aquela que afeta vários componentes de um sistema.

Para maximizar a reutilização do código de uma lógica comercial complexa, normalmente tendemos a projetar uma hierarquia de classes em torno das funções de negócios primárias e de base do sistema. Mas e quanto às outras questões que não são específicas de negócios que abrangem a hierarquia de classes? Onde você encaixaria recursos como armazenamento em cache, segurança e registro em log? É muito provável que eles acabem sendo repetidos em cada objeto afetado.

Por não ser de responsabilidade específica de um determinado componente ou família de componentes, uma questão abrangente é um aspecto do sistema que deve ser trabalhado em um nível lógico diferente, além das classes de aplicativo. Por esse motivo, anos atrás foi definido um paradigma de programação diferenciado: a programação orientada a aspecto (AOP). Incidentemente, o conceito de AOP foi desenvolvido nos laboratórios da Xerox PARC na década de 1990. A equipe também desenvolveu a primeira (e ainda mais popular) linguagem de AOP: AspectJ.

Embora quase todo mundo concorde com os benefícios da AOP, ela ainda não foi amplamente implementada. Na minha opinião, o principal motivo de uma adoção limitada como essa é basicamente a falta de ferramentas apropriadas. Tenho certeza de que o dia em que a AOP tiver (mesmo que apenas parcialmente) suporte nativo no Microsoft .NET Framework será um divisor de águas na história dessa linguagem. Hoje, só é possível fazer AOP no .NET usando estruturas ad hoc.

A ferramenta mais potente para AOP no .NET é o PostSharp, disponível em sharpcrafters.com. O PostSharp oferece uma completa estrutura de AOP que permite conhecer todos os principais recursos da teoria de AOP. Todavia, deve-se observar que muitas estruturas de injeção de dependência (DI) incluem alguns recursos de AOP.

Por exemplo, você encontra recursos de AOP no Spring.NET, Castle Windsor e, claro, no Microsoft Unity. Para cenários relativamente simples, como rastreamento, armazenamento em cache e decoração de componentes na camada de aplicativo, os recursos de estruturas de DI normalmente dão conta do recado. No entanto, é difícil optar pelas estruturas de DI quando o assunto é objetos de domínio e objetos de interface do usuário. Uma questão abrangente pode sem dúvida ser vista como dependência externa, e as técnicas de DI certamente permitem injetar dependências externas em uma classe.

A questão é que a DI provavelmente exigirá um design positivo ad hoc ou uma dose de refatoração. Em outras palavras, se você já estiver usando uma estrutura de DI, é fácil trazer alguns recursos de AOP. Por outro lado, se o seu sistema não tem a DI, trazer uma estrutura de DI poderá ser um pouco trabalhoso. Isso nem sempre será possível em um projeto grande ou durante a atualização de um sistema herdado. Usando uma abordagem clássica de AOP, você inclui qualquer questão abrangente em um novo componente chamado aspecto. Neste artigo, primeiro apresentarei uma rápida visão geral do paradigma de orientação a aspecto e depois passarei para os recursos relacionados a AOP disponíveis no Unity 2.0.

Um guia rápido de AOP

Um projeto de programação orientada a objeto é formado por uma série de arquivos de origem, sendo que cada um implementa uma ou mais classes. O projeto também inclui classes que representam questões abrangentes, como registro em log ou armazenamento em cache. Todas as classes são processadas por um compilador e produzem código executável. Na AOP, um aspecto é um componente reutilizável que encapsula o comportamento exigido por muitas classes no projeto. A forma como cada aspecto é processado depende da tecnologia de AOP que você está considerando. De um modo geral, podemos dizer que os aspectos não são simples e diretamente processados pelo compilador. Existe uma outra ferramenta específica de tecnologia que é necessária para modificar o código executável para se considerar os aspectos. Vamos analisar rapidamente o que acontece com o AspectJ, um compilador de AOP em Java que foi a primeira ferramenta de AOP criada.

Com o AspectJ, você usa a linguagem de programação Java para escrever classes e a linguagem AspectJ para escrever aspectos. O AspectJ dá suporte a uma sintaxe personalizada pela qual você indica o comportamento esperado do aspecto. Por exemplo, um aspecto de registro em log pode especificar que registrará em log antes e depois que um determinado método for chamado. De um certo modo, os aspectos são mesclados no código-fonte normal e produzem uma versão intermediária do código-fonte que será compilado em um formato executável. No jargão do AspectJ, o componente que pré-processa aspectos e os mescla com o código-fonte é chamado de weaver. Ele produz uma saída que o compilador pode processar em um executável.

Em suma, um aspecto descreve uma parte do código reutilizável que você deseja injetar em classes existentes sem tocar no código-fonte dessas classes. Em outras estruturas de AOP (como o .NET PostSharp), você não encontrará uma ferramenta weaver. Porém, o conteúdo de um aspecto sempre é processado pela estrutura e resulta em alguma forma de injeção de código.

Nesse sentido, injeção de código é diferente de injeção de dependência. Injeção de código refere-se à capacidade de uma estrutura de AOP inserir chamadas a pontos de extremidade públicos no aspecto em pontos específicos dentro do corpo de classes decoradas com um dado aspecto. A estrutura PostSharp, por exemplo, permite escrever aspectos como atributos .NET que depois você anexa aos métodos das classes. Os atributos PostSharp são processados pelo compilador do PostSharp (poderíamos até chamá-lo de weaver) em uma etapa posterior à compilação. O resultado é que seu código é aprimorado para incluir parte do código nos atributos. Mas os pontos de injeção são resolvidos automaticamente, e tudo o que você precisa fazer como desenvolvedor é escrever um componente de aspecto autossuficiente e anexá-lo a um método de classe pública. É fácil escrever e mais fácil ainda manter o código.

Para finalizar esta rápida visão geral sobre AOP, vou apresentar alguns termos específicos e explicar o significado deles. Um ponto de junção indica um ponto no código-fonte da classe de destino em que você deseja injetar o código do aspecto. Um pointcut representa uma coleção de pontos de junção. Um advice refere-se ao código a ser injetado na classe de destino. O código pode ser injetado antes, depois e ao redor do ponto de junção. Um advice está associado a um pointcut. Esses termos vêm da definição original de AOP e podem não se refletir literalmente na estrutura de AOP específica que você está usando. É recomendável que você tente captar os conceitos por detrás dos termos (os pilares da AOP) e use esse conhecimento para entender melhor os detalhes de uma determinada estrutura.

Um guia rápido do Unity 2.0

O Unity é um bloco de aplicativo disponível no projeto Microsoft Enterprise Library e também como um download à parte. O Microsoft Enterprise Library é uma coleção de blocos de aplicativo que lida com várias questões abrangentes próprias do desenvolvimento de aplicativos no .NET: registro em log, armazenamento em cache, criptografia, tratamento de exceções, entre outros. A versão mais recente do Enterprise Library é a 5.0, lançada em abril de 2010 e fornecida com suporte total para o Visual Studio 2010 (saiba mais sobre ela no Patterns & Practices do Developer Center, em msdn.microsoft.com/library/ff632023).

O Unity é um dos blocos de aplicativo do Enterprise Library. Também disponível para Silverlight, o Unity é basicamente um contêiner de DI com suporte adicional para um mecanismo de intercepção por meio do qual você pode tornar as suas classes um pouco mais orientadas a aspecto.

Intercepção no Unity 2.0

No Unity, a ideia central da intercepção é permitir que os desenvolvedores personalizem a cadeia de chamadas necessárias para invocar um método em um objeto. Em outras palavras, o mecanismo de intercepção do Unity capturas as chamadas que estão sendo feitas para objetos configurados e personaliza o comportamento dos objetos de destino, acrescentando um código extra antes, depois ou ao redor da execução usual de métodos. Intercepção é basicamente uma abordagem extremamente flexível para adicionar um novo comportamento a um objeto no tempo de execução sem tocar em seu código-fonte nem afetar o comportamento de classes no mesmo caminho de herança. A intercepção do Unity é uma forma de implementar o padrão Decorator, um padrão de design popular que visa estender a funcionalidade de um objeto no tempo de execução à medida que o objeto é utilizado. Um decorador é um objeto contêiner que recebe (e mantém uma referência a) uma instância do objeto de destino e aumenta seus recursos para o mundo externo.

O mecanismo de intercepção do Unity 2.0 dá suporte à intercepção de instância e de tipo. Além disso, a intercepção funciona independentemente do modo como o objeto é instanciado, seja ele uma instância conhecida ou criado através do contêiner do Unity. No último caso, você só pode usar outra API, completamente autônoma. No entanto, nesse caso, você perde o suporte ao arquivo de configuração. A Figura 1 mostra a arquitetura do recurso de intercepção do Unity, detalhando como ele funciona em uma determinada instância de objeto não resolvida por meio do contêiner. (A figura é apenas uma versão um pouco reelaborada de uma figura que você encontra na documentação do MSDN.)

Figura 1 Intercepção de objeto em funcionamento no Unity 2.0

O subsistema de intercepção é formado por três elementos-chave: o interceptor (ou proxy), o pipeline de comportamento e o comportamento, ou aspecto. Nas duas pontas do subsistema estão o aplicativo cliente e o objeto de destino, isto é, o objeto ao qual estão sendo atribuídos comportamentos adicionais não inseridos no código-fonte. Uma vez que o aplicativo cliente está configurado para usar a API de intercepção do Unity em uma dada instância, qualquer invocação de método passa por um objeto proxy — o interceptor. Esse objeto proxy vê a lista de comportamentos registrados e os chama pelo pipeline interno. Cada comportamento configurado recebe uma oportunidade de executar antes ou depois da chamada regular do método de objeto. O proxy injeta dados de entrada no pipeline e recebe qualquer valor de retorno como inicialmente gerado pelo objeto de destino e, depois, modificado adicionalmente por comportamentos.

Configurando a intercepção

A forma recomendada de usar a intercepção no Unity 2.0 é diferente das versões anteriores, embora a abordagem usada em versões anteriores seja totalmente suportada para fins de compatibilidade com versões anteriores. No Unity 2.0, a intercepção é apenas uma nova extensão que você adiciona ao contêiner para descrever como um objeto é resolvido. Este é o código necessário para configurar a intercepção via código fluente:

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

O contêiner precisa encontrar informações sobre os tipos a serem interceptados e os comportamentos a serem adicionados. Essas informações podem ser adicionadas usando o código fluente ou via configuração. Considero a configuração particularmente flexível, uma vez que ela permite modificar as coisas sem tocar no aplicativo e sem a necessidade de uma nova etapa de compilação. Vejamos a abordagem baseada em configuração.

Para começar, adicione o seguinte ao arquivo de configuração:

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

A finalidade desse script é estender o esquema de configuração com novos elementos e aliases específicos do subsistema de intercepção. Outra adição adequada é a seguinte:

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

Para conseguir o mesmo efeito usando código fluente, chame AddNewExtension<T> e RegisterType<T> no objeto contêiner.

Vamos examinar o script de configuração mais detalhadamente. O elemento <extension> adiciona intercepção ao contêiner. Observe que a “Intercepção” que está sendo usada no script é um dos aliases definidos na extensão da seção. O tipo de interface IBankAccount é mapeado para o tipo concreto BankAccount (esse é o trabalho clássico de um contêiner de DI) e associado a um determinado tipo de interceptor. O Unity oferece dois tipos principais de interceptores: interceptores de instância e interceptores de tipo. No mês que vem, vou me aprofundar mais nos interceptores. Por ora, basta dizer que um interceptor de instância cria um proxy para filtrar as chamadas de entrada direcionadas à instância interceptada. Os interceptores de tipo, por sua vez, apenas imitam o tipo do objeto interceptado e trabalham em uma instância de um tipo derivado. (Para obter mais informações sobre interceptores, consulte msdn.microsoft.com/library/ff660861(PandP.20).)

O interceptor de interface é um interceptor de instância que se limita a funcionar como o proxy somente de uma interface no objeto. O interceptor de interface usa a geração de código dinâmico para criar a classe de proxy. O elemento do comportamento de intercepção na configuração indica o código externo que você deseja executar em torno da instância do objeto interceptado. A classe TraceBehavior deve ser configurada de forma declarativa para que o contêiner possa resolvê-la e quaisquer uma de suas dependências. Use o elemento <register> para instruir o contêiner sobre a classe TraceBehavior e seu construtor esperado, como mostrado aqui:

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

A Figura 2 mostra um trecho da classe TraceBehavior.

Figura 2 Um exemplo de comportamento do Unity

class TraceBehavior : IInterceptionBehavior, IDisposable
{
  private TraceSource source;

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

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

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

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

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

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

   public bool WillExecute
   {
     get { return true; }
   }

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

Uma classe de comportamento implementa IInterceptionBehavior, que basicamente consiste no método Invoke. Esse método contém toda a lógica que você deseja usar para qualquer método sob o controle do interceptor. Se você quiser fazer algo antes da chamada do método de destino, faça-o no começo do método. Quando você quiser recorrer ao objeto de destino, ou mais precisamente, ao próximo comportamento registrado no pipeline, chame o representante getNext fornecido pela estrutura. Para terminar, você pode usar o código que quiser para o pós-processamento do objeto de destino. O método Invoke precisa retornar uma referência ao próximo elemento no pipeline; se for retornado nulo, a cadeia será interrompida e outros comportamentos nunca serão chamados.

Flexibilidade de configuração

A intercepção e, de um modo mais geral, a AOP, trabalham com inúmeros cenários interessantes. A intercepção permite, por exemplo, adicionar responsabilidades a objetos individual sem modificar a classe inteira, mantendo a solução muito mais flexível do que seria com um decorador.

Este artigo abordou apenas superficialmente a aplicação da AOP ao .NET. Nos próximos meses, escreverei mais sobre intercepção no Unity e a AOP de um modo geral.

Dino Esposito é o autor de "Programming Microsoft ASP.NET MVC", publicado pela Microsoft Press (2010) e coautor de “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2008). Residente na Itália, Esposito é um palestrante sempre presente em eventos do setor no mundo inteiro. Entre em contato com ele através de seu blog, em weblogs.asp.net/despos.

Meus agradecimentos aos seguinte especialista técnico para revisar este artigo: Chris Tavares