Este artigo aborda a melhora de performance de aplicações, através da utilização de cache. É apresentado um componente para cache que utiliza conceitos de AOP (Aspect Oriented Programming), sendo facilmente plugável a qualquer sistema já existente ou novo, com baixo nível de acoplamento. Além de AOP, também veremos no decorrer do artigo conceitos de orientação a objetos e os Design Patterns Strategy, Template Method e Simple Factory.


Guia do artigo:

O cache serve para guardar dados temporariamente em memória, evitando acessos recorrentes ao seu meio de armazenamento original (banco de dados, disco ou outro recurso), de forma que a obtenção da informação seja mais rápida. O componente para cache apresentado serve para centralizar e padronizar a forma que os nossos sistemas lidam com este assunto.

Sistemas em geral (Web, Web Services, Remoting, Windows Services, WCF, Desktop, etc.) que fazem acessos frequentes à base de dados ou outro recurso para recuperação de informações, podem se beneficiar da utilização do cache para melhora de performance e redução da taxa de transferência de dados de rede.

Neste artigo veremos como melhorar a performance de aplicações através da utilização de cache. Para tal, vamos contextualizar o conceito de cache e sua aplicação em sistemas de informação. Em seguida definiremos os requisitos de um componente para centralizar e padronizar o mecanismo de cache e explicaremos o conceito de programação orientada a aspectos. Com os requisitos em mãos, partiremos para a estruturação da arquitetura e finalmente para o desenvolvimento e teste do componente.

Podemos encontrar diversos artigos a respeito de melhora de performance de aplicações. Em grande parte deles nota-se um consenso quanto à utilização de um recurso, o cache. Como exemplo, temos o artigo de Rob Howard: 10 Tips for Writting High-Performance Web Applications, disponível no site MSDN (veja a sessão links), que aponta a utilização de cache de formas diferentes, direta ou indiretamente em quatro das dez dicas apresentadas.

Mas afinal o que é cache e por que ele é importante para a melhora de performance? Em uma definição geral, o cache é um meio de acesso rápido, onde podem ser guardadas informações temporariamente para posterior consulta, como alternativa a acessá-las diretamente em seu meio de armazenamento padrão – que normalmente apresenta tempo de acesso à informação mais demorado do que o do cache. Na prática, utilizamos o cache em nossos sistemas para guardar em memória as informações que precisamos acessar com frequência, evitando a busca recorrente destas informações em outros recursos que podem apresentar tempo de resposta mais demorado, como por exemplo, um arquivo em disco, um sistema gerenciador de banco de dados disponível em outro servidor ou outro dispositivo disponível na rede ou internet.

Um dos cenários em que o cache torna-se um fator importante para a melhora de performance é o de sistemas que consultam dados com frequência em recursos distribuídos, como é o caso das aplicações que utilizam SGBDs (sistemas gerenciadores de bancos de dados) localizados fisicamente em servidor separado ao da aplicação. A utilização do cache em um sistema deste tipo ajuda a reduzir a taxa de transferência de dados de rede, favorecendo de maneira geral todos os sistemas que utilizam a mesma rede. Ajuda também a melhorar o tempo de processamento e resposta do sistema, uma vez que alguns dados poderão estar disponíveis em memória local, evitando sua busca através da rede e posterior processamento no SGBD, o que por sua vez também colabora com a diminuição da carga no SGBD, beneficiando todas as aplicações que o utilizam.

A implementação de um mecanismo de cache simples poderia se dar através de uma estrutura que permita guardar dados em memória, acessíveis através de um valor chave. Por exemplo, se desejarmos guardar em cache uma lista de clientes, poderíamos ter o código identificador do cliente como chave de acesso ao item do cache e todos os demais dados como o seu conteúdo. Desde as suas primeiras versões, o .NET Framework possui recursos que nos permitem implementar facilmente um mecanismo de cache, dentre os diversos tipos de projetos que podemos construir: Web Applications, Web Services, .NET Remoting, Windows Services, WCF, Windows Applications, etc. Podemos citar como exemplo, a criação de um mecanismo através da utilização de variáveis do tipo Dictionary (System.Collections.Generic.Dictionary) , que nos permite manter uma lista de qualquer tipo de dado, possibilitando acesso através de uma chave única. Podemos também utilizar o objeto Cache (System.Web.Caching.Cache) do ASP .NET (caso estejamos desenvolvendo aplicações Web), disponível no .NET Framework justamente para este fim.

Porém, quando formos construir uma aplicação pensando na utilização de cache, podemos nos deparar com diversas questões, como por exemplo: Em qual camada do meu sistema devo tratar o cache? Como posso fazer para que meu código que trata de regras de negócios, persistência de dados ou mesmo da apresentação, não fique repleto de códigos que tratam especificamente do cache de dados e suas particularidades, tornando-se altamente acoplado ao mecanismo de cache que escolhemos? Como implementar um mecanismo de cache facilmente plugável aos sistemas já existentes? Como deixar este mecanismo configurável, de forma que seja possível, dentre outras configurações, ativá-lo e desativá-lo facilmente?

Nota do DevMan

Acoplamento é um conceito de design de software que se refere ao quanto as classes ou subsistemas estão interconectados ou dependem uns dos outros. Dizer que uma classe possui alto acoplamento com um componente externo significa dizer que a classe conhece detalhes do componente e em boa parte do seu código há refêrencias para ele. Devemos projetar componentes pensando no menor nível de acoplamento possível, tornando o código mais modularizado, o que o torna menos complexo e favorece evoluções futuras.

Veremos a seguir, como criar um componente para tratar o cache em nossas aplicações, de forma a endereçar todas estas questões.

Identificando os requisitos do componente

Vamos começar o processo de estruturação do componente pela definição dos principais requisitos que queremos que ele atenda:

  • RQ01 - O componente deverá ser facilmente plugável a sistemas já existentes: Queremos construir um componente que possa ser facilmente plugado tanto em sistemas novos quanto em sistemas existentes. Desta forma, poderemos criar novos sistemas performáticos e também poderemos melhorar a performance dos sistemas legados através da inclusão do componente criado;
  • RQ02 - O componente deverá apresentar baixo nível de acoplamento com os sistemas em que for utilizado: Não é desejável que para implementar o cache em um sistema, seja necessário adicionar várias linhas de código em suas classes, de forma que o sistema fique altamente acoplado ao mecanismo de cache;
  • RQ03 - O mecanismo de cache deverá ser transparente para o sistema em que for implementado: Desejamos que o componente não interfira no objetivo chave da classe onde ele for implementado. Ou seja, se o componente for referenciado na camada de persistência da aplicação, é desejável que a sua implementação não tenha que ser intercalada com os demais comandos que recuperam e tratam as informações do banco de dados;
  • RQ04 - O componente deverá ser estendível: O componente deverá ser construído de forma que o repositório onde as informações serão guardadas temporariamente possa ser facilmente alterado, permitindo assim que diferentes repositórios possam ser utilizados para diferentes cenários de utilização do componente. A princípio, o componente deverá suportar utilização nos ambientes Web e Windows, sendo necessária a escolha dos repositórios apropriados para cada um dos ambientes;
  • RQ05 - O componente deverá ser configurável: Uma vez plugado em um sistema, o componente deve permitir parametrização através do arquivo de configurações da aplicação (app.config para aplicações Windows e web.config para aplicações Web);
  • RQ06 – O componente deverá permitir as seguintes configurações: Quantidade de milissegundos, após a inclusão do dado no cache, para que seja considerado como expirado; Intervalo de tempo em milissegundos em que o componente buscará e removerá itens expirados no cache; Ativação ou desativação da funcionalidade de cache;
  • RQ07 – O componente deverá permitir armazenamento de qualquer tipo de dado em cache: Tipos primitivos, demais tipos disponíveis no .Net Framework, bem como tipos criados pelo desenvolvedor deverão ser suportados. Uma vez armazenados, os dados deverão ser acessíveis através de uma chave de acesso;
  • RQ08 – O componente deverá efetuar limpezas periódicas no repositório de cache: Até mesmo os dados pouco voláteis (com baixa frequência de mudança) podem sofrer alterações. Para possibilitar que estas alterações sejam refletidas nas aplicações que utilizam o mecanismo de cache, os dados devem ser renovados periodicamente no repositório do cache. O componente deverá então, ter um mecanismo de limpeza periódica, possibilitando que dados modificados sejam atualizados também no cache e que dados utilizados com pouca frequência não ocupem espaço no cache desnecessariamente.

Definidos os requisitos, vamos tratar nos tópicos seguintes da solução técnica para endereçá-los.

Endereçando os requisitos

Para criarmos um componente facilmente plugável, que apresente baixo nível de acoplamento com o sistema que o utilize e cuja utilização seja transparente (RQ01, RQ02 e RQ03), vamos utilizar recursos de programação orientada a aspectos conhecidos como AOP (aspect oriented programming).

Simplificadamente a AOP refere-se à injeção de aspectos (trechos de código / comportamentos comuns) a um código existente, de forma que seu comportamento seja alterado, porém, seu código original fique intacto. Como exemplo, imagine um método que recupere informações de um banco de dados. Podemos, através da AOP, incluir funcionalidades de log, tratamento de exceções, cache de dados, tracing e outros, sem alterarmos uma linha sequer do código já existente.

Nota: Veremos nos tópicos seguintes como utilizar a AOP através da utilização do PostSharp, um framework que facilita a utilização da AOP com .Net. Tanto a AOP quanto o framework PostSharp são temas extremamente abrangentes e por isto não serão aprofundados neste artigo. Caso você tenha interesse em saber mais sobre o assunto, a 59ª edição da .net magazine traz o artigo AOP – Utilizando Orientação a Aspectos em .NET, o qual é um bom ponto de partida. Adicionalmente, neste artigo estão listados alguns links sobre os principais sites sobre o assunto (veja a sessão links).

A Listagem 1 apresenta um exemplo de código onde é empregado o conceito de AOP através do framework PostSharp. Note que a forma de injetar a funcionalidade é realizada através da inclusão de uma classe atributo no método. Quando o código é compilado o PostSharp faz a inclusão do código correspondente ao atributo, no método em que ele foi empregado. Desta forma podemos acrescentar funcionalidades aos nossos métodos de forma transparente, simples e com baixo nível de acoplamento.

Listagem 1. Exemplo de método com classe atributo.

  [DevMedia.Performance.Cache.Cache.CacheAttribute()]
  private static Int32 OperacaoSoma(Int32 a, Int32 b)
  {
    return a + b;
  }

Seguindo este conceito, criaremos uma estrutura de classes que permitirá incluir a funcionalidade de cache através da inclusão de uma classe atributo. A Figura 1 ilustra esta estrutura, compondo o pacote Cache do nosso componente. A classe CacheAttribute é a classe que marcará os métodos com o recurso de cache. Note que ela herda da classe OnMethodBoundaryAspect, que é a classe do PostSharp usada para criação de aspectos que podem ser atrelados a métodos. Veremos mais detalhes a respeito desta classe mais adiante.

Diagrama de classes do pacote Cache
Figura 1. Diagrama de classes do pacote Cache.

Para permitir que o repositório do cache seja estendível (RQ04), utilizaremos dois design patterns. O Strategy, onde definimos uma estrutura abstrata/interface e implementações concretas desta estrutura, de forma que o sistema dependa da estrutura abstrata ou interface e as implementações concretas variem sem afetá-lo. E o Simple Factory, criando uma classe que será responsável por criar instâncias das possíveis implementações concretas de repositório de cache.

Nota: O Simple Factory não é um design pattern concebido pelo GoF (Gang of Four), grupo que estabeleceu uma lista de padrões amplamente reconhecidos, classificados como padrões de criação, estruturais e comportamentais. Porém, o Simple Factory não deixa de ser um padrão de design, também sendo amplamente reconhecido e utilizado pelo mercado. O site dofactory (veja a sessão links) é uma boa referência para diversos padrões, inclusive os design patterns criados pelo GoF.

Desta forma, poderemos ter diversas implementações de diferentes repositórios de cache (respeitando uma mesma interface ou classe base abstrata), e uma classe factory, responsável pela escolha e instanciação de uma delas. O critério de escolha quanto a qual implementação concreta instanciar pode ser implementado através de uma regra hard-coded na classe factory ou baseada em configurações feitas no arquivo de configurações da aplicação, por exemplo.

Toda a estrutura responsável por tratar o repositório do cache ficará organizada dentro de um pacote denominado CacheRepository. A Figura 2 ilustra como será a estrutura de classes deste pacote, atendendo aos design patterns estabelecidos. Note que temos uma classe abstrata denominada CacheRepository, onde todas as operações comuns aos repositórios de cache serão definidas. Esta classe possui dependência com a classe CacheItem, que encapsula um item mantido em cache. As classes CacheRepositoryWeb e CacheRepositoryDictionary são as possíveis implementações concretas de cache que teremos no componente. Ambas herdam a estrutura da classe abstrata CacheRepository. A classe CacheRepositoryFactory é a classe responsável por criar uma instância da classe CacheRepositoryWeb ou CacheRepositoryDictionary. Ela possui dependência com a classe CacheRepository, pois possuirá um método que retornará uma instância de uma classe deste tipo (CacheRepositoryWeb ou CacheRepositoryDictionary).

Note que através desta estrutura podemos facilmente adicionar um novo repositório de cache ao componente, bastando para tal criar a classe concreta que encapsula o novo repositório, herdando da classe base CacheRepository e alterando a classe CacheRepositoryFactory para referenciá-la. Nenhuma outra alteração seria necessária nas demais estruturas do componente, desta forma atendendo ao requisito RQ04.

Diagrama de classes do pacote CacheRepository
Figura 2. Diagrama de classes do pacote CacheRepository.

Para que o componente seja configurável (RQ05), criaremos um pacote dentro do componente denominado CacheSettings. Este pacote será responsável por encapsular as questões relacionadas às configurações do componente. Basicamente ele conterá a classe Settings, que lerá as configurações do arquivo de configurações da aplicação onde o componente está sendo utilizado (seja a aplicação Web - web.config – ou Windows – app.config). A Figura 3 ilustra a estrutura descrita.

Diagrama de classes do pacote CacheSettings
Figura 3. Diagrama de classes do pacote CacheSettings.

Neste ponto já endereçamos os requisitos RQ01, RQ02, RQ03, RQ04 e RQ05, já sendo possível estabelecer a estrutura do componente. Veja esta estrutura na Figura 4, composta pelos pacotes Cache, CacheRepository e CacheSettings. Já estamos prontos para iniciar a implementação do componente, endereçando os requisitos restantes (RQ06, RQ07 e RQ08). No tópico seguinte veremos como configurar o ambiente e posteriormente veremos como se dará a implementação.

Diagrama de pacotes ilustrando a estrutura do componente

Figura 4. Diagrama de pacotes ilustrando a estrutura do componente.

Montando o ambiente

Como ponto de partida, devemos instalar o framework Postsharp para podermos utilizar suas DLLs no nosso projeto. Para tal, acesse o site do Postsharp (veja a sessão links) e faça o seu download. Neste artigo está sendo utilizada a versão 1.5.6.629-Release-x86. Em seguida faça a sua instalação, descompactando o arquivo e executando o instalador (msi) contido nele. A instalação é simples, não sendo necessária nenhuma configuração adicional.

Instalado o Postsharp, criaremos um novo projeto do tipo Class Library no Microsoft Visual C# 2008 Express Edition (Figura 5), onde será implementado o componente.

Criação do projeto do tipo Class Library, onde
será implementado o componente
Figura 5. Criação do projeto do tipo Class Library, onde será implementado o componente.

Em seguida, adicionaremos à solução um novo projeto do tipo Console Application (Figura 6 e Figura 7), onde realizaremos o teste do componente.

Nota: Para a implementação do componente apresentado neste artigo foi utilizado o Microsoft Visual C# 2008 Express Edition. O código da implementação é perfeitamente compatível com o Microsoft Visual Studio 2005 (.Net Framework 2.0). Note, porém, que em outras versões do Visual Studio poderão haver diferenças nos passos que envolvem seleção de funcionalidades da ferramenta.

Adicionar novo projeto à solução
Figura 6. Adicionar novo projeto à solução.
Criação do projeto do tipo Console Application
Figura 7. Criação do projeto do tipo Console Application, onde implementaremos os testes.

Criados os dois projetos, remova a classe Class1.cs criada por padrão dentro do projeto do tipo Class Library e adicione a ele a referência para as DLLs do PostSharp. Se a instalação do PostSharp foi bem sucedida, as DLLs PostSharp.Laos e PostSharp.Public devem aparecer na aba .NET, conforme demonstrado na Figura 8. No decorrer da implementação também utilizaremos recursos que estão disponíveis nos componentes System.Configuration e System.Web. Por padrão, estes componentes não são referenciados quando criamos um novo projeto, sendo necessário incluirmos as referências, conforme também demonstrado na Figura 8.

Adicionando as referências necessárias
Figura 8. Adicionando as referências necessárias.

Por falar em referência, também precisaremos de uma referência no projeto DevMedia.Performance.Cache.Test para o componente que vamos criar (DevMedia.Performance.Cache), de forma que consigamos utilizá-lo neste projeto para a realização dos testes. Como o componente DevMedia.Performance.Cache possui referências para as DLLs do PostSharp, para utilizá-lo precisamos adicionar as referências para estas DLLs também no projeto consumidor. Adicione então ambas as referências, conforme demonstrado na Figura 9.

Adicionando a referência para o componente no projeto de testes
Figura 9. Adicionando a referência para o componente no projeto de testes.

Como vimos no tópico anterior, o componente será configurável e se baseará no arquivo de configurações da aplicação em que ele está sendo utilizado. Precisaremos então de um arquivo de configurações no Console Application que criamos para os testes. Para tal, adicione um novo item no projeto DevMedia.Performance.Cache.Test do tipo Application Configuration File, com o nome app.config, conforme demonstrado na Figura 10.

Adicionando o arquivo de configurações no
projeto de testes
Figura 10. Adicionando o arquivo de configurações no projeto de testes.

Por fim, vamos criar três pastas dentro do projeto do tipo Class Library, representando os pacotes que definimos no tópico anterior. As pastas deverão receber os nomes dos pacotes que definimos (Cache, CacheSettings e CacheRepository), organizando o componente em namespaces com os mesmos nomes. Após a execução dos passos descritos, a solução final deverá ficar como demonstrado na Figura 11.

Solução pronta para a implementação
Figura 11. Solução pronta para a implementação.

Implementando o pacote CacheSettings

Configurado o ambiente, vamos iniciar a implementação pelo pacote CacheSettings, pois ele é o pacote mais simples e não possui dependências com os demais. Conforme vimos na Figura 3, este pacote possui apenas uma classe, a classe Settings. Vamos então adicionar uma nova classe dentro da pasta CacheSettings, com o nome Settings. Nesta classe implementaremos três propriedades que representarão as configurações estabelecidas no requisito RQ06: RecycleInterval (intervalo de tempo em que será executada a varredura do cache para exclusão de itens expirados), ExpirationInterval (o intervalo de tempo padrão, decorrido após a inclusão do item no cache, para que ele seja considerado como expirado) e CacheEnabled (define se o recurso de cache estará ou não ativo na aplicação). Estas propriedades encapsularão o acesso às entradas do arquivo de configurações do sistema em que o componente foi plugado. Caso a configuração não seja realizada, as propriedades retornarão valores padrão. Confira na Listagem 2 o código correspondente a este mecanismo.

Listagem 2. Implementação da classe Settings.

  using System;
  using System.Configuration;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
   
  namespace DevMedia.Performance.Cache.CacheSettings
  {
      /// <summary>
      /// Classe responsável por encapsular acessos à configuração do componente. Por padrão
      /// acessa o arquivo de configuração da aplicação, buscando por chaves com nomes
      /// pré-definidos. Em caso de não existência das chaves, assume valores padrões.
      /// </summary>
      public class Settings
      {
          //Constantes que definem os nomes das chaves de configuração, no arquivo de configuração
          private const string STR_CHAVE_CONFIG_INTERVALO_EXCLUSAO_CACHE = "CacheIntervaloVarreduraExclusaoItens";
          private const string STR_CHAVE_CONFIG_INTERVALO_EXPIRACAO = "CacheIntervaloExpiracaoItem";
          private const string STR_CHACE_CONFIG_CACHE_ATIVADO = "CacheAtivado";
   
          /// <summary>
          /// Retorna o intervalo de tempo em milissegundos em que o componente verificará por itens expirados no cache 
          /// para remoção
          /// </summary>
          public static int RecycleInterval
          {
              get
              {
                  if (ConfigurationManager.AppSettings[STR_CHAVE_CONFIG_INTERVALO_EXCLUSAO_CACHE] == null == false)
                  {
                      return Convert.ToInt32(ConfigurationManager.AppSettings[STR_CHAVE_CONFIG_INTERVALO_EXCLUSAO_CACHE]);
                  }
                  else
                  {
                      //1 hora em milissegundos (1 hora * 60 minutos * 60 segundos * 1000 milissegundos)
                      return 3600000;
                  }
              }
          }
   
          /// <summary>
          /// Retorna o intervalo de tempo em milissegundos em que os itens serão expirados por padrão. 
          /// </summary>
          public static int ExpirationInterval
          {
              get
              {
                  if (ConfigurationManager.AppSettings[STR_CHAVE_CONFIG_INTERVALO_EXPIRACAO] == null == false)
                  {
                      return Convert.ToInt32 (ConfigurationManager.AppSettings[STR_CHAVE_CONFIG_INTERVALO_EXPIRACAO]);
                  }
                  else
                  {
                      //12 horas em milissegundos (12 horas * 60 minutos * 60 segundos * 1000 milissegundos)
                      return 43200000;
                  }
              }
          }
   
          /// <summary>
          /// Retorna se a funcionalidade de cache está ou não ativa
          /// </summary>
          public static bool CacheEnabled
          {
              get
              {
                  if (ConfigurationManager.AppSettings[STR_CHACE_CONFIG_CACHE_ATIVADO] == null == false)
                  {
                      return Convert.ToBoolean(ConfigurationManager.AppSettings[STR_CHACE_CONFIG_CACHE_ATIVADO]);
                  }
                  else
                  {
                      return false;
                  }
              }
          }
      }
  }

Observe na Listagem 2 que as constantes definem os nomes das chaves de configuração que deverão estar presentes no arquivo de configurações da aplicação. Sendo assim, vamos implementar estas configurações no arquivo app.config criado dentro do projeto DevMedia.Performance.Cache.Test. Conforme pode ser observado na Listagem 3, as chaves devem ser adicionadas dentro da sessão appSettings do arquivo de configurações.

Com exceção da configuração CacheEnabled (configurada como ativo no arquivo de configurações e como desativada por padrão na classe Settings), manteremos as demais configurações com os valores de acordo com o estabelecido como padrão na classe Settings, contando com a possibilidade de alterá-los facilmente caso necessário.

Listagem 3. Implementação do arquivo de configurações app.config.

  <?xml version="1.0" encoding="utf-8" ?>
  <configuration>
    <appSettings >
      <!-- Configura o intervalo de tempo em milissegundos em que o componente verificará por itens expirados 
           no cache para remoção.
           Configurado como: 1 hora em milissegundos (1 hora * 60 minutos * 60 segundos * 1000 milissegundos) -->
      <add key="CacheIntervaloVarreduraExclusaoItens" value ="3600000"/>
      <!-- Configura o intervalo de tempo em milissegundos em que os itens serão considerados como expirados por padrão no 
           cache. 
           Configurado como: 12 horas em milissegundos (12 horas * 60 minutos * 60 segundos * 1000 milissegundos) -->
      <add key="CacheIntervaloExpiracaoItem" value="43200000"/>
      <!-- Ativa ou desativa funcionalidade de cache 
           true=Ativado, false=Desativado -->
      <add key="CacheAtivado" value="true"/>
    </appSettings>
  </configuration>

Implementando o pacote CacheRepository

O próximo passo é a implementação das classes que compõem o pacote CacheRepository, responsável por agrupar as classes que tratam da estrutura de armazenamento dos itens em cache. Conforme podemos observar na Figura 2, este pacote é composto pelas classes CacheItem, CacheRepository, CacheRepositoryWeb, CacheRepositoryDictionary e CacheFactory. Crie estas classes dentro da pasta CacheRepository, no projeto DevMedia.Performance.Cache. Iniciaremos pela definição da classe CacheItem, responsável por abstrair um item armazenável em cache.

Um item armazenável em cache deve possuir uma chave de acesso que o possibilite ser identificado de forma única (atendendo ao requisito RQ07), uma data/hora que defina sua expiração (atendendo ao requisito RQF08) e um objeto que guarde qualquer tipo de dado que se deseje armazenar em cache (atendendo ao requisito RQ07). Conforme podemos observar na Listagem 4, implementaremos esta estrutura através de variáveis privadas e propriedades públicas que encapsularão o acesso a estas informações.

Para facilitar as implementações que utilizarão esta classe, criaremos também três construtores que permitirão informar a chave, o dado e a data de expiração que serão guardados pelo objeto criado. Note na Listagem 4 que o que muda entre os três construtores é a forma de configurar a data de expiração. O primeiro permite informar uma data de expiração explicita (por exemplo: 21/01/2010 17:00:00). O segundo construtor permite informar um intervalo de tempo relativo à data atual, através de um TimeSpan (se a data atual é 21/10/2010 15:00:00 e o TimeSpan informado corresponde a duas horas, a data de expiração assumida será 21/10/2010 17:00:00). Já no terceiro construtor a data de expiração não é informada, e o componente assume o valor DateTime.MaxValue, de forma que um item criado com este construtor nunca expirará.

Note que para simplificar, estamos usando o tipo object para definir o objeto que guardará o dado armazenado em cache. Poderíamos utilizar recursos do Generics para permitir que a classe CacheItem fosse tipável, assumindo o tipo definido para o objeto do cache. Fica aqui a dica para melhoria.

Listagem 4. Implementação da classe CacheItem.

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
   
  namespace DevMedia.Performance.Cache.CacheRepository
  {
      /// <summary>
      /// Classe responsável por abstrair um item armazenável em cache. Contém a chave de acesso,
      /// o item guardado em cache e sua data de expiração.
      /// </summary>
      public class CacheItem
      {
          //Variáveis privadas onde serão guardadas as informações expostas através
          //das propriedades
          private string strKey;
          private object objItem;
          private System.DateTime dtaExpirationDate;
   
          /// <summary>
          /// Construtor que permite a especificação da data exata de expiração do item
          /// </summary>
          public CacheItem(string strKey, object objItem, System.DateTime dtaExpirationDate)
          {
              this.strKey = strKey;
              this.objItem = objItem;
              this.dtaExpirationDate = dtaExpirationDate;
          }
   
          /// <summary>
          /// Construtor que permite a especificação de um TimeSpan a ser somado
          /// à data atual, para definição da data de expiração do item
          /// </summary>
          public CacheItem(string strKey, object objItem, TimeSpan spnExpirationSpan)
          {
              this.strKey = strKey;
              this.objItem = objItem;
              this.dtaExpirationDate = System.DateTime.Now.Add(spnExpirationSpan);
          }
   
          /// <summary>
          /// Construtor que não permite especificar a data de expiração do item.
          /// A data de expiração é assumida com o valor default DateTime.MaxValue.
          /// </summary>
          public CacheItem(string strKey, object objItem)
          {
              this.strKey = strKey;
              this.objItem = objItem;
              this.dtaExpirationDate = System.DateTime.MaxValue;
          }
   
          /// <summary>
          /// Chave de acesso ao item do cache
          /// </summary>
          public string Key
          {
              get { return strKey; }
              set { strKey = value; }
          }
   
          /// <summary>
          /// Objeto armazenado em cache
          /// </summary>
          public object Item
          {
              get { return objItem; }
              set { objItem = value; }
          }
   
          /// <summary>
          /// Data de expiração da validade do item armazenado em cache
          /// </summary>
          public System.DateTime ExpirationDate
          {
              get { return dtaExpirationDate; }
              set { dtaExpirationDate = value; }
          }
      }
  }

Continuando a implementação do pacote CacheRepository, o próximo passo é a implementação da classe de mesmo nome do pacote, CacheRepository. Conforme podemos observar na Figura 2, A classe CacheRepository é uma classe abstrata que define o contrato para as implementações concretas de repositório de Cache.

Nota do DevMan

Uma classe abstrata difere de uma interface pelo fato de poder ter implementações em métodos e propriedades. A principal característica dela é o fato de não poder ser instanciada diretamente, diferindo-a também de uma classe comum. A finalidade da classe abstrata é ter implementações concretas das características comuns a qualquer classe que herde dela e ter definições dos métodos e propriedades que devem ser implementados (sobrescritos) diretamente nas classes herdeiras. Desta forma, podemos implementar todo o código comum na classe abstrata e deixar os códigos específicos para as classes que a herdarão, evitando assim a repetição de código.

Neste contrato, devemos então definir as operações que qualquer repositório de cache concreto deve implementar. Basicamente, um repositório de cache deve permitir adicionar, remover, verificar a existência de um item, retornar o conteúdo de um item e permitir a navegação através dos itens contidos no cache. Devemos lembrar que as operações de inclusão e remoção dos itens em cache não devem gerar erro caso o item a ser incluso já exista no cache ou caso o item que está sendo removido já tenha sido removido previamente. Também é uma boa ideia que estas operações suportem utilização por aplicações multi-threading, o que implica em garantir que as operações que manipulam os itens em cache tenham bloqueios de código para que apenas uma thread o execute por vez (thread safe). Além disso, conforme estabelecido no requisito RQ08, deveremos ter no nosso repositório de cache um mecanismo para remoção dos itens expirados.

Para as operações de inclusão e exclusão, vamos criar dois métodos Add (adicionar um item ao cache) e Remove (remover um item do cache). Estes métodos terão implementações para o tratamento das consistências mencionadas e para o tratamento da utilização por aplicações multi-threading. Como a inclusão e a remoção efetiva no repositório concreto deverão ser feitas pelas classes concretas, faremos chamadas nestes métodos para outros dois métodos abstratos correspondentes a estas funcionalidades, chamados de AddItem e RemoveItem. Esta forma de implementação segue o design-pattern Template Method, que define a criação do esqueleto de um algoritmo em uma operação (métodos Add e Remove), delegando alguns passos para as subclasses (métodos AddItem e RemoveItem).

Os métodos AddItem e RemoveItem deverão ser protegidos, o que garantirá que apenas as classes que herdarem desta terão acesso a eles e que as inclusões e remoções sejam acionadas apenas através dos métodos Add e Remove.

Já para permitir o acesso a um item do cache, vamos criar uma propriedade indexada, onde dado uma chave de item, seja retornado o seu conteúdo. Como a busca e retorno do item do cache é uma tarefa específica das implementações concretas, criaremos esta propriedade como abstrata.

Para permitir a navegação através dos itens existentes no cache, criaremos a propriedade CacheEnumerator, responsável pela obtenção de um enumerador que permita navegação através dos itens do repositório de cache. Da mesma forma que a propriedade indexada, esta propriedade também será abstrata.

Nota do DevMan

Um enumerador é um objeto que permite a navegação através de uma coleção por meio do método MoveNext. As coleções que implementam a interface IEnumerable expõem o método GetEnumerator, responsável por retornar um enumerador do tipo IEnumerator. Conforme veremos mais adiante, as coleções utilizadas neste artigo para armazenamento dos itens em cache implementam a interface IEnumerable, o que nos permite utilizar este recurso.

E finalmente, para atender ao requisito RQ08, criaremos um objeto timer nesta classe, responsável pela verificação periódica quanto à existência de itens expirados no cache, fazendo a sua remoção. Este timer deverá ser instanciado no construtor da classe, garantindo que ele seja ativado assim que o cache for utilizado pela primeira vez na aplicação. Ainda no construtor, configuraremos o timer quanto ao intervalo de tempo em que ocorrerão as iterações de execução, de acordo com o configurado na aplicação (RQ05 e RQ06).

Faremos então a varredura dos itens expirados, no método que trata o evento Elapsed do timer. Neste método navegaremos em todo o repositório do cache através do seu enumerador, e para cada item do cache obteremos a sua chave e o incluiremos em uma lista de itens a remover, caso a sua data de expiração seja anterior a data atual. Em seguida, a lista de itens a remover será percorrida, e o método de remoção será chamado para os itens contidos nela. Note que alguns métodos e propriedades referenciados por este são abstratos, o que caracteriza o conceito do Template Method novamente.

Confira na Listagem 5 como ficou todo este mecanismo.

Listagem 5. Implementação da classe abstrata CacheRepository.

  using System;
  using System.Collections;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
   
  namespace DevMedia.Performance.Cache.CacheRepository
  {
      /// <summary>
      /// Classe abstrata, responsável por definir o contrato base para uma implementação
      /// de um repositório de cache concreto. Define os métodos padrão, bem como implementa funcionalidade
      /// básica que todas as implementações concretas deverão herdar.
      /// </summary>
      public abstract class CacheRepository
      {
          //Timer responsável por efetuar a limpeza do cache em intervalos de tempo configurável
          private static System.Threading.Timer tmrRecycle;
   
          /// <summary>
          /// Construtor responsável por instânciar o timer, já configurando-o com o intervalo de tempo
          /// de execução
          /// </summary>
          public CacheRepository()
          {
              //Como o timer é estático, só o instanciamos caso não tenha sido instanciado anteriormente
              if (tmrRecycle == null)
              {
                  tmrRecycle = new System.Threading.Timer(tmrRecycle_Elapsed, null, 
  CacheSettings.Settings.RecycleInterval, CacheSettings.Settings.RecycleInterval);
              }            
          }
   
          /// <summary>
          /// Adiciona o item passado como parâmetro no cache
          /// </summary>
          public void Add(CacheItem objCacheItem)
          {
              lock (tmrRecycle)
              {
                  if (ContainsKey(objCacheItem.Key) == false)
                  {
                      AddItem(objCacheItem);
                  }
                  else
                  {
                      this[objCacheItem.Key] = objCacheItem;
                  }
              }
          }
   
          /// <summary>
          /// Remove o item passado como parâmetro do cache
          /// </summary>
          public void Remove(CacheItem objCacheItem)
          {
              lock (tmrRecycle)
              {
                  if (ContainsKey(objCacheItem.Key))
                  {
                      RemoveItem(objCacheItem);
                  }
              }
          }
   
          /// <summary>
          /// Método abstrato que deverá ser implementado pela classe concreta que herdar desta. É chamado
          /// pelo método concreto e público Add.
          /// </summary>
          protected abstract void AddItem(CacheItem objCacheItem);
   
          /// <summary>
          /// Método abstrato que deverá ser implementado pela classe concreta que herdar desta. É chamado
          /// pelo método concreto e público Remove.
          /// </summary>
          protected abstract void RemoveItem(CacheItem objCacheItem);
   
          /// <summary>
          /// Método abstrato que deverá ser implementado pela classe concreta que herdar desta. É responsável
          /// pela verificação quanto a existência ou não da chave passada como parâmetro no cache.
          /// </summary>
          public abstract bool ContainsKey(string strKey);
   
          /// <summary>
          /// Propriedade indexada responsável por retornar um item do repositório de cache
          /// dado uma chave
          /// </summary>
          public abstract CacheItem this[string strKey]
          {
              set;
              get;
          }
   
          /// <summary>
          /// Propriedade readonly, responsável por retornar um enumerador que permite acesso
          /// a navegação através dos itens em cache.
          /// </summary>
          public abstract IDictionaryEnumerator CacheEnumerator
          {
              get;
          }
   
          /// <summary>
          /// Evento disparado pelo timer responsável pela limpeza de itens expirados do cache.
          /// Varre o cache em busca de itens expirados efetuando a sua remoção.
          /// </summary>
          private void tmrRecycle_Elapsed(object objState)
          {
              IDictionaryEnumerator enumerator = CacheEnumerator;
              string key = null;
              List<CacheItem> itemsToRemove = new List<CacheItem>();
   
              lock (tmrRecycle)
              {
                  while (enumerator.MoveNext())
                  {
                      key = null;
   
                      if (enumerator.Current is KeyValuePair<string, object>)
                      {
                          key = ((KeyValuePair<string, object>)enumerator.Current).Key;
                      }
                      else if (enumerator.Current is DictionaryEntry)
                      {
                          key = Convert.ToString(((DictionaryEntry)enumerator.Current).Key);
                      }
   
                      if (key == null == false && ContainsKey(key) && this[key].ExpirationDate <= System.DateTime.Now)
                      {
                          itemsToRemove.Add(this[key]);
                      }
                  }
   
                  foreach (CacheItem item in itemsToRemove)
                  {
                      Remove(item);
                  }
              }
          }
      }
  }

Atendendo ao requisito RQ04, o próximo passo é a implementação das classes concretas CacheRepositoryWeb e CacheRepositoryDictionary, ilustradas na Figura 2. A classe CacheRepositoryWeb será responsável por manter um mecanismo de armazenamento de dados em cache para aplicações Web, enquanto a classe CacheRepositoryDictionary será responsável por manter um mecanismo de armazenamento de dados para aplicações Windows. Temos esta diferenciação, pois, para aplicações Web, temos a possibilidade de utilizar o repositório de cache já existente no .Net Framework, a classe System.Web.Caching.Cache, enquanto que para aplicações Windows teremos que utilizar um mecanismo que produza efeito semelhante, um objeto do tipo System.Collections.Generic.Dictionary<string, object>.

Ambas as classes herdam da classe CacheRepository, herdando as consistências contidas nela e o mecanismo de limpeza do cache, especializando as operações concretas mencionadas anteriormente. Confira na Listagem 6 as implementações concretas de cada repositório. Note que o que diferencia uma da outra, basicamente, é o tipo do atributo objCache. No restante da classe estamos apenas implementando as operações abstratas da classe CacheRespository, de forma que estamos chamando os métodos correspondentes do atributo objCache.

Listagem 6. Implementação das classes concretas CacheRepositoryWeb e CacheRepositoryDictionary.

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
   
   
  namespace DevMedia.Performance.Cache.CacheRepository
  {
      /// <summary>
      /// Implementação concreta de repositório de cache. Tem como intuito ser utilizada por
      /// aplicações desktop, armazenando os dados do cache em uma coleção do tipo Dictionary, 
      /// mantendo-os na memória local do terminal onde o componente está sendo utilizado.
      /// </summary>
      internal class CacheRepositoryDictionary : CacheRepository
      {
          //Variável onde serão armazenados os itens em cache
          private Dictionary<string, object> objCache;
   
          //Construtor responsável por instanciar o repositório do cache
          public CacheRepositoryDictionary() : base()
          {
              objCache = new Dictionary<string, object>();
          }
   
          //Implementação concreta do método de inclusão de itens no cache
          protected override void AddItem(CacheItem objCacheItem)
          {
              objCache.Add(objCacheItem.Key, objCacheItem);
          }
   
   
          //Implementação concreta do método de remoção de itens no cache
          protected override void RemoveItem(CacheItem objCacheItem)
          {
              objCache.Remove(objCacheItem.Key);
          }
   
          //Implementação concreta do método responsável pela verificação da existência ou não de um
          //item no cache, através da sua chave
          public override bool ContainsKey(string strKey)
          {
              return objCache.ContainsKey(strKey);
          }
   
   
          //Implementação concreta da propriedade responsável pelo retorno do enumerador 
          //do repositório do cache
          public override System.Collections.IDictionaryEnumerator CacheEnumerator
          {
              get { return objCache.GetEnumerator(); }
          }
   
          //Implementação concreta da propridade indexada responsável por permitir acesso
          //aos itens do cache através da sua chave
          public override CacheItem this[string strkey]
          {
              get { return (CacheItem)objCache[strkey]; }
              set { objCache[strkey] = value; }
          }
      }
  }
   
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using System.Web.Caching;
   
  namespace DevMedia.Performance.Cache.CacheRepository
  {
   
      /// <summary>
      /// Implementação concreta de repositório de cache. Tem como intuito ser utilizada por
      /// aplicações WEB, armazenando os dados do cache na implementação de cache padrão do ASP .NET, 
      /// o ASP .NET Cache.
      /// </summary>
      internal class CacheRepositoryWeb : CacheRepository
      {
          //Variável onde serão armazenados os itens em cache
          private System.Web.Caching.Cache objCache;
   
          //Construtor responsável por instânciar o repositório do cache
          public CacheRepositoryWeb() : base()
          {
              objCache = System.Web.HttpContext.Current.Cache;
          }
   
          //Implementação concreta do método de inclusão de itens no cache
          protected override void AddItem(CacheItem objCacheItem)
          {
              objCache.Add(objCacheItem.Key, objCacheItem, null,
                      System.Web.Caching.Cache.NoAbsoluteExpiration,
                      System.Web.Caching.Cache.NoSlidingExpiration,
                      CacheItemPriority.Normal,
                      null);
          }
   
          //Implementação concreta do método de remoção de itens no cache
          protected override void RemoveItem(CacheItem objCacheItem)
          {
              objCache.Remove(objCacheItem.Key);
          }
   
          //Implementação concreta do método responsável pela verificação da existência ou não de um
          //item no cache, através da sua chave
          public override bool ContainsKey(string strKey)
          {
              return (objCache[strKey] == null == false) && (objCache[strKey]) is CacheItem;
          }
   
          //Implementação concreta da propriedade responsável pelo retorno do enumerador 
          //do repositório do cache
          public override System.Collections.IDictionaryEnumerator CacheEnumerator
          {
              get { return objCache.GetEnumerator(); }
          }
   
          //Implementação concreta da propridade indexada responsável por permitir acesso
          //aos itens do cache através da sua chave
          public override CacheItem this[string strKey]
          {
              get { return (CacheItem)objCache[strKey]; }
              set { objCache[strKey] = value; }
          }
      }
  }

Para fechar a implementação do pacote CacheRepository, precisamos implementar o mecanismo que ficará responsável por criar as instâncias dos repositórios concretos do componente, deixando o resto do componente desacoplado com as implementações concretas. Conforme vimos na Figura 2, este mecanismo será implementado através da classe CacheRepositoryFactory.

Basicamente criaremos nesta classe um método que retornará um objeto do tipo CacheRepository (a classe abstrata que criamos anteriormente). Este método decidirá de acordo com o contexto em que o componente está sendo utilizado se instanciará um objeto do tipo CacheRepositoryWeb ou CacheRepositoryDictionary. Para identificarmos o contexto em que o componente está sendo utilizado, podemos verificar se há um contexto HTTP presente, através do objeto System.Web.HttpContext.Current. Se este objeto não for nulo significa que está sendo utilizada uma aplicação web e, portanto, deveremos retornar uma instância da classe CacheRepositoryWeb. Caso contrário, retornaremos uma instância da classe CacheRepositoryDictionary.

Como o repositório de cache será único durante todo o ciclo de vida da aplicação, esta classe também deverá ser responsável por manter apenas uma instância ativa de um dos repositórios, de forma que, caso ele já tenha sido instanciado anteriormente, a instância pré-existente seja retornada. Para garantirmos a instância única, temos que prever que se o método for chamado simultaneamente (cenários multi-threading), apenas um acesso ao trecho de código que cria a instância será permitido por vez. Criaremos então um método estático chamado GetCacheRepository, onde implementaremos todo este mecanismo (Listagem 7).

Listagem 7. Implementação da classe CacheRepositoryFactory.

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
   
  namespace DevMedia.Performance.Cache.CacheRepository
  {
      /// <summary>
      /// Classe factory que implementa lógica para definir qual implementação de cache
      /// será utilizada, de acordo com o tipo da aplicação consumidora do componente. Na versão corrente,
      /// retorna implementação padrão para aplicações desktop e implementação baseada no ASP .NET cache
      /// para aplicações Web.
      /// </summary>
      internal class CacheRepositoryFactory
      {
          //Objeto utilizado para bloqueio de acesso à alguns trechos de código
          private static object objLock = new object();
   
          //Possíveis repositórios de cache mantidos por esta factory
          private static CacheRepositoryWeb objCacheWeb;
          private static CacheRepositoryDictionary objCacheDic;
   
          /// <summary>
          /// Retorna uma instância de um objeto do tipo Cache, dependendo do tipo da aplicação que o invoca.
          /// Caso trate-se de aplicação web, cria componente que encapsula ASP.Net Cache,
          /// caso contrário, retornar objeto que implementa cache padrão.
          /// </summary>
          public static CacheRepository GetCacheRepository()
          {
              if (System.Web.HttpContext.Current == null == false)
              {
                  //Efetua o bloqueio para evitar que mais de uma thread
                  //execute o mesmo trecho de código ao mesmo tempo, possibilitando 
                  //a criação de mais de uma variável de cache.
                  lock ((objLock))
                  {
                      if (objCacheWeb == null)
                      {
                          objCacheWeb = new CacheRepositoryWeb();
                      }
                  }
   
                  return objCacheWeb;
              }
              else
              {
                  //Efetua o bloqueio para evitar que mais de uma thread
                  //execute o mesmo trecho de código ao mesmo tempo, possibilitando 
                  //a criação de mais de uma variável de cache.
                  lock ((objLock))
                  {
                      if (objCacheDic == null)
                      {
                          objCacheDic = new CacheRepositoryDictionary();
                      }
                  }
   
                  return objCacheDic;
              }
          }
      }
  }

Implementando o pacote Cache

Neste ponto talvez você esteja se perguntando: mas afinal, como funciona este mecanismo de cache? Até o momento criamos o repositório de cache e o mecanismo que encapsula suas configurações. Falta agora a parte central do componente, o mecanismo responsável por controlar e fornecer a funcionalidade de cache para as aplicações em que o componente será utilizado.

Este mecanismo funcionará da seguinte maneira. A classe CacheAttribute (pertencente ao pacote Cache, conforme vimos na Figura 1), marcará (conforme vimos no exemplo da Listagem 1) os métodos que quisermos que tenham a funcionalidade de cache. Estes métodos poderão ser métodos de busca de dados, implementados em classes da camada de acesso a dados da aplicação, por exemplo. Quando um método de busca de informações, marcado com o atributo CacheAttribute for acionado por qualquer outro ponto da aplicação (um método na camada de regras de negócios, por exemplo), o código existente na classe CacheAttribute será executado antes do código do método acionado. Este código verificará se este mesmo método já foi executado anteriormente, com os mesmos parâmetros informados. Esta verificação será feita no repositório de cache que implementamos anteriormente (pacote CacheRepository). Caso ainda não tenha sido, a classe CacheAttribute permitirá a execução do método original e ao término, a classe CacheAttribute será acionada novamente e desta vez ela guardará no cache, o método executado e seus parâmetros como a chave de acesso (propriedade Key da classe CacheItem) e o seu retorno como o conteúdo (propriedade Item da classe CacheItem). Por outro lado, caso o método já tenha sido executado anteriormente, a classe CacheAttribute apenas obterá o conteúdo armazenado no cache e o retornará, interrompendo a execução do método original, poupando o tempo da sua execução.

A Figura 12 demonstra o processo descrito através de um diagrama de sequência. Note que o objetivo do diagrama apresentado não é representar a implementação física, mas sim, representar em alto nível a lógica que está envolvida na operação. No diagrama, a classe ClasseConsumidoraDaPersistencia representa qualquer classe em nosso sistema que execute um método de retorno de informações de uma classe de persistência de dados. No exemplo, esta classe está chamando a operação RetornarCliente da classe ClassePersistência, informando o código do cliente como parâmetro.

Diagrama de sequência ilustrando o
funcionamento do mecanismo de cache
Figura 12. Diagrama de sequência ilustrando o funcionamento do mecanismo de cache.

Voltando agora para a implementação física, vamos partir para a criação da classe CacheAttribute. Crie uma classe com este nome dentro da pasta Cache, no projeto DevMedia.Performance.Cache.

Conforme falamos anteriormente, esta classe herdará da classe OnMethodBoundaryAspect do PostSharp. A classe OnMethodBoundaryAspect nos permite sobrescrever alguns eventos que são disparados quando o método marcado com ela é acionado, de forma que consigamos fazer implementações que serão executadas antes e depois da execução do método, conforme vimos na Figura 12. Os eventos disponíveis são OnEntry (disparado quando o método é acionado, antes da sua execução), OnExit (disparado quando o método é terminado) e OnSuccess (disparado quando o método é terminado sem exceções). Veremos a implementação destes eventos mais adiante. Por hora, vamos continuar com a estruturação da classe.

Em se tratando de uma classe atributo para métodos, ela não pode ser herdada, por isto ela será marcada como sealed. Uma classe atributo pode ser usada em outras classes (como é o caso da classe Serializable do .Net Framework, por exemplo) ou em métodos (como é o caso da classe WebMethod, também do .Net Framework). No caso desta classe que estamos criando, ela só poderá ser usada em métodos e apenas uma vez por método. Veja na Listagem 8 como faremos todas estas configurações.

Listagem 8. Definição da classe CacheAttribute.

  using System;
  using System.Reflection;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
   
  using PostSharp.Laos;
  using PostSharp.Extensibility;
   
  namespace DevMedia.Performance.Cache.Cache
  {
      /// <summary>
      /// Atributo atrelável a métodos cujos dados podem ou devem serem guardados em cache, visando 
      /// melhora de performance na aplicação.
      /// </summary>
      /// <remarks>Deve ser utilizada com cautela, uma vez que os dados ocuparão espaço em memória,
      /// sendo recomendável sua utilização em métodos implementados na camada DAL, cujos dados retornados 
      /// sejam preferencialmente dados de domínio com baixo índice de alteração.</remarks>
      [Serializable(), AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
      public sealed class CacheAttribute : OnMethodBoundaryAspect
      {     
      } 
  }

Dentro da nossa classe precisamos de um atributo que nos permita acessar o repositório de cache, pois, conforme vimos na Figura 12, a classe CacheAttribute manipula o repositório. É importante entender que toda vez que um método com o atributo CacheAttribute for executado, será criada uma nova instância da classe CacheAttribute. Porém, precisamos que o repositório de cache seja único em toda a aplicação. Para endereçarmos esta questão, criaremos o atributo como estático, de forma a ser único, independentemente de quantas instâncias do CacheAttribute existirem. Inicializaremos o atributo com o retorno método GetCacheRepository, da classe CacheRepositoryFactory, discutida anteriormente. A inicialização será feita no construtor da classe CacheAttribute, que será acionado assim que qualquer método marcado com ela seja acionado. Por fim, tudo isto só terá sentido se o mecanismo de cache estiver configurado como ativo (RQF06) na aplicação, por isso, colocaremos uma verificação no construtor. Confira o código correspondente na Listagem 9.

Listagem 9. Criando e instanciando o repositório de cache no CacheAttribute.

         /// <summary>
          /// Repositório onde serão armazenados os retornos dos métodos invocados. 
          /// Trata-se de uma classe abstrata que será concretizada no construtor através de uma factory.
          /// </summary>
          public static CacheRepository.CacheRepository cache;
   
          /// <summary>
          /// Construtor do atributo de cache. É acionado assim que um método marcado 
          /// com esta classe é acionado.
          /// </summary>
          static CacheAttribute()
          {
              if (CacheSettings.Settings.CacheEnabled)
              {
                  cache = CacheRepository.CacheRepositoryFactory.GetCacheRepository();
              }
          }

Vamos então sobrescrever a implementação dos eventos OnEntry e OnSuccess, onde implementaremos a lógica para manipulação do cache. Tanto o evento OnEntry como o OnSuccess recebem como parâmetro uma variável do tipo PostSharp.Laos.MethodExecutionEventArgs. Com este parâmetro conseguimos obter informações do método acionado (o método que marcamos com a classe CacheAttribute), além de também conseguirmos interferir na sua execução, dizendo se ela será continuada ou interrompida e até definindo o objeto que será retornado por ele.

Como vimos, no tratamento do evento OnEntry temos que compor a chave do cache com o método executado e seus parâmetros e em seguida pesquisar no repositório por uma execução anterior com a mesma chave. Para isto, vamos criar o método GetComposedKey, para o qual passaremos como parâmetro o mesmo parâmetro recebido no evento OnEntry, a variável do tipo PostSharp.Laos.MethodExecutionEventArgs. Este método retornará uma string correspondente a assinatura do método e os parâmetros recebidos em um formato padronizado, de forma que seja único para cada conjunto de parâmetros informados. A classe MethodExecutionEventArgs nos disponibiliza a propriedade Method e o método GetReadOnlyArgumentArray, responsáveis por retornar a assinatura do método chamado e obter os parâmetros informados respectivamente. Vamos usar ambos neste método que vamos criar. Confira a implementação na Listagem 10.

Nota: Para simplificar o exemplo, no método GetComposedKey não estamos tratando métodos com recursos do Generics nem métodos que recebam como parâmetro dados que não implementem o método ToString(), o que poderia causar duplicidades de chave no cache, caso seja usado em métodos com estas características. O site do PostSharp (veja a sessão links) possui um ótimo exemplo de como fazer o tratamento completo. Fica aqui a dica para melhoria.

Listagem 10. Implementando o método para composição da chave do cache.

          /// <summary>
          /// Método responsável pela composição da chave do cache. Baseia-se na assinatura
          /// do método chamado e nos parâmetros recebidos.
          /// </summary>
          private string GetComposedKey(MethodExecutionEventArgs eventArgs)
          {
              StringBuilder sb = new StringBuilder();
   
              //Inclui a assinatura do método
              sb.Append(eventArgs.Method.ToString());
   
              //Abre o parênteses que cercará os parâmetros recebidos
              sb.Append("(");
   
              //Para cada parâmetro recebido, inclui seu valor
              object[] arguments = eventArgs.GetReadOnlyArgumentArray();
              for (int i = 0; i <= arguments.Length - 1; i++)
              {
                  if (i > 0) { sb.Append(","); };
   
                  sb.Append(arguments[i].ToString());
              }
   
              //Fecha o parênteses dos parâmetros e retorna a string montada
              sb.Append(")");
              return sb.ToString(); 
          }

O próximo passo é a criação do método que tratará o evento OnEntry. Nele, chamaremos o método GetComposedKey para obter a chave do cache e verificaremos se há um item no cache correspondente a esta chave. Havendo, interferiremos na execução do método original, retornando o conteúdo do cache como seu retorno. Para isto, vamos usar as propriedades ReturnValue e FlowBehavior do objeto eventArgs do tipo MethodExecutionEventArgs, recebido como parâmetro. A propriedade ReturnValue nos permite definir qual objeto será retornado pelo método e o FlowBehavior dizer qual fluxo deverá ser assumido (retorno imediato ou continuidade, que é o padrão). Caso não exista um item com a chave pesquisada no cache, o método original assumirá o fluxo de execução normal. Não podemos esquecer que todo este código só deverá ser executado se a funcionalidade do cache estiver configurada como ativa na aplicação. Confira esta implementação na Listagem 11.

Listagem 11. Implementando o tratamento do evento OnEntry.

       /// <summary>
          /// Executado em tempo de execução, antes da execução do método em que está configurado
          /// este atributo
          /// </summary>
          /// <param name="eventArgs">Parâmetros/informações do método</param>
          public override void OnEntry(MethodExecutionEventArgs eventArgs)
          {
              //Verifica se a funcionalidade de cache está ou não ativa, nas configurações da aplicação
              if (CacheSettings.Settings.CacheEnabled)
              {
                  // Compõe a chave do cache
                  string key = GetComposedKey(eventArgs);
   
                  // Verifica se a chamada ao método já se encontra em cache
                  if ((cache.ContainsKey(key))) {
                      // Caso esteja no cache, setamos o item do cache como o retorno do método
                      // e forçamos o método a retornar imediatamente.
                      eventArgs.ReturnValue = cache[key].Item;
                      eventArgs.FlowBehavior = FlowBehavior.Return;
                  }
              }
          }

Por fim, vamos implementar o tratamento do evento OnSuccess, onde inseriremos o retorno do método executado no cache para posterior utilização. Para isto, vamos montar a chave que será usada para identificar o item no cache através do método GetComposedKey, vamos obter o intervalo de expiração que o novo item deverá assumir através da classe Settings, pacote CacheSettings, implementado anteriormente e vamos acionar o método Add da variável cache. Mais uma vez, só realizaremos estes passos se a funcionalidade de cache estiver configurada como ativa na aplicação. Confira esta implementação na Listagem 12.

Listagem 12. Implementando o tratamento do evento OnSuccess.

          /// <summary>
          /// Executado em tempo de execução, após a execução do método em que está configurado
          /// este atributo
          /// </summary>
          /// <param name="eventArgs">Parâmetros/informações do método</param>
          public override void OnSuccess(MethodExecutionEventArgs eventArgs)
          {
              //Verifica se a funcionalidade de cache está ou não ativa, nas configurações da aplicação
              if (CacheSettings.Settings.CacheEnabled)
              {
                  //Obtém a chave que foi configurada no método OnEntry.
                  string key = GetComposedKey(eventArgs);
   
                  //Obtém o intervalo de expiração definido no arquivo de configurações
                  TimeSpan expirationInterval = new TimeSpan(0, 0, 0, 0, CacheSettings.Settings.ExpirationInterval);
   
                  //Adiciona o item no cache
                  cache.Add(new CacheRepository.CacheItem(key, eventArgs.ReturnValue, expirationInterval));                
              }
          }

Com isto finalizamos a implementação do componente. Vamos testar?

Realizando testes

Para testarmos o componente criado, vamos implementar um método que simulará a busca de um cliente na base de dados, dado um código passado como parâmetro. Para simplificar o exemplo, este método não fará acesso a uma base de dados, mas sim, a cada vez que for executado gerará um delay de três segundos e em seguida retornará uma string que representará um cliente localizado. Desta forma conseguiremos ver claramente quando o método é executado e quando o cliente já está no cache e é retornado sem que o método seja executado.

No método Main da classe Program (projeto DevMedia.Performance.Cache.Test), faremos uma implementação para informarmos o código do cliente que queremos pesquisar, mostrando o tempo gasto na pesquisa. Confira esta implementação na Listagem 13.

Listagem 13. Testando o componente de cache.

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
   
  namespace DevMedia.Performance.Cache.Test
  {
      class Program
      {
          static void Main(string[] args)
          {
              //Variáveis de trabalho
              string opt = "S";
              string cliente;
              Int32 codigoCliente;
   
              while (opt.ToUpper() == "S")
              {
                  //Obtendo código do cliente a ser pesquisado
                  Console.WriteLine();
                  Console.WriteLine("---------------------------------------------------------------");
                  Console.Write("Digite o código do cliente: ");
                  while (!Int32.TryParse(Console.ReadLine(), out codigoCliente))
                  {
                      Console.Write("Digite o código do cliente (apenas valores numéricos): ");
                  }
                  
                  //Informando data e hora antes da operação
                  Console.WriteLine(String.Format("Iniciando operação de busca às {0}", 
                          DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss")));
   
                  //Acionando método com atributo de cache
                  cliente = RetornarCliente(codigoCliente);
   
                  //Informando data e hora após a operação
                  Console.WriteLine(String.Format("Cliente localizado............ {0}", 
                      DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss")));
   
                  //Apresentando o cliente localizado
                  Console.WriteLine(String.Format("Cliente: {0}", cliente));
                  Console.WriteLine();               
   
                  //Perguntando se deseja realizar novo teste
                  opt = string.Empty;
                  while (!(opt.ToUpper().Equals("S") | opt.ToUpper().Equals("N")))
                  {
                      Console.Write("Deseja realizar novamente (S/N)?");
                      opt = Console.ReadLine();
                  }
   
                  Console.WriteLine("---------------------------------------------------------------");
              }
          }
   
          /// <summary>
          /// Método que simula o funcionamento de um método da camada de persistência da aplicação
          /// </summary>
          [DevMedia.Performance.Cache.Cache.CacheAttribute()]
          private static string RetornarCliente(Int32 codigoCliente)
          {
              //Gerando delay de três segundos, simulando o tempo que o método
              //demoraria para buscar um dado no banco de dados, quando este não estiver no cache
              System.Threading.Thread.Sleep(3000);
   
              //Retornando um cliente fictício
              return String.Format("Cliente {0}", codigoCliente);
          }
      }
  }

Vamos então executar o projeto de testes e analisar os resultados. Vamos informar o mesmo código de cliente duas vezes e verificar se na segunda execução o resultado é obtido em menos de três segundos, o que indicará que o dado foi colocado em cache após a primeira execução. Em seguida, repetiremos os mesmos passos para outro código. Veja a simulação na Figura 13.

Resultados dos testes
Figura 13. Resultados dos testes.

Analisando os resultados, podemos concluir que a segunda e a quarta operação não envolveram a execução do método RetornarCliente, mas sim, pararam na execução do método OnEntry que implementamos na classe CacheAttribute, onde o dado é buscado no cache e o fluxo de execução é desviado para o retorno imediato do dado. O componente de cache está funcionando!

Vamos agora testar o mecanismo de limpeza do cache implementado, alterando as configurações da aplicação para que as varreduras sejam executadas a cada segundo e para que os itens sejam considerados como expirados após dez segundos de permanência no cache. Altere as chaves do arquivo app.config (projeto DevMedia.Performance.Cache.Test), conforme demonstrado na Listagem 14.

Listagem 14. Alterando as configurações para testar a limpeza do cache.

  <?xml version="1.0" encoding="utf-8" ?>
  <configuration>
    <appSettings >
      <!-- Configura o intervalo de tempo em milissegundos em que o componente verificará por itens expirados 
           no cache para remoção.
           Configurado como: 1 segundo em milissegundos (1 segundo * 1000 milissegundos) -->
      <add key="CacheIntervaloVarreduraExclusaoItens" value ="1000"/>
      <!-- Configura o intervalo de tempo em milissegundos em que os itens serão considerados como expirados por padrão no 
           cache. 
           Configurado como: 10 segundos em milissegundos (10 segundos * 1000 milissegundos) -->
      <add key="CacheIntervaloExpiracaoItem" value="10000"/>
      <!-- Ativa ou desativa funcionalidade de cache 
           true=Ativado, false=Desativado -->
      <add key="CacheAtivado" value="true"/>
    </appSettings>
  </configuration>

Alteradas as configurações, vamos realizar um novo teste. Desta vez utilizaremos um mesmo código de cliente em quatro pesquisas. A primeira deverá levar três segundos para retornar e a segunda deverá retornar imediatamente. Aguardaremos então cerca de dez segundos para testar pela terceira vez e verificaremos se a busca leva três segundos novamente, o que indicará que a limpeza foi efetuada. Realizaremos o quarto teste apenas para confirmar que após o terceiro, o dado foi colocado no cache novamente. Confira na Figura 14 os resultados.

Resultados dos testes – Limpeza do cache
Figura 14. Resultados dos testes – Limpeza do cache.

Analisando os resultados, verificamos que a limpeza do cache funcionou exatamente como esperado! O dado foi removido do cache após cerca de dez segundos e foi incluso novamente no teste posterior. Realize outros testes com outros valores e depure o código para entender melhor todo o funcionamento. Veja também como é simples desativar e ativar a funcionalidade do cache alterando a configuração da aplicação.

Conclusão

Um mecanismo de caching de dados pode ajudar na melhora da performance das nossas aplicações à medida que guardarmos em memória, para acesso posterior, os dados que acessamos com frequência em recursos distribuídos ou de acesso custoso. Porém, quando formos implementar uma aplicação com este recurso, é importante levarmos em consideração, dentre outros fatores, o tipo do dado que iremos armazenar no cache, a quantidade de tempo suportável pelo negócio para o consumo de dados desatualizados (uma vez que enquanto em cache, o dado atualizado no recurso originário não será refletido para a aplicação) e o quanto de memória dispomos para o armazenamento dos dados.

Dados voláteis ou sensíveis para o negócio podem não ser bons candidatos para armazenamento em cache. Podemos considerar a possibilidade de guardarmos em cache apenas informações de domínio com baixa frequência de atualização, já tendo algum ganho de performance. Podemos também projetar tempos de expiração dos dados e frequências de limpeza que ajudem a minimizar este fator.

Quanto ao consumo de memória, podemos estimar e planejar os recursos de infraestrutura necessários para suportar o sistema. Podemos também considerar a possibilidade de direcionar o repositório do cache para um servidor dedicado a este fim, diminuindo o tempo do processamento para a busca do dado no local originário, porém, consumindo recursos de rede. Existem diversas alternativas. O importante é projetar antes de implantar!

A programação orientada a aspectos pode nos ajudar com diversos fins. Podemos criar componentes semelhantes para tratamento de exceções, geração de logs e traces e diversas outras operações que normalmente deixam nossos códigos repletos de trechos que tratam outros que não o fim principal. O site do PostSharp possui algumas ideias, não deixe de visitá-lo! Até o próximo artigo!

Confira também