Usando o padrão Decorator (GoF)

O padrão Decorator, do livro Design Patterns: Elements of Reusable Object-Oriented Software, é útil quando precisamos adicionar um comportamento em uma classe já existente. Vamos demonstrar isso através de um exemplo de uma classe de acesso ao banco de dados onde será adicionado o uso de cache.

Primeiro temos  nossa classe que acessará o "banco de dados". Na verdade, a nossa base nada mais é que um array em memória. Optei por fazer assim para manter a simplicidade do exemplo e focarmos no que realmente é importante. Além disso, a busca demora propositadamente 3 segundos para simularmos todo o trabalho de se acessar um recurso externo à aplicação como um banco de dados.

// RepositorioConfiguracaoImpl.cs
using System.Collections.Generic;
using System.Threading;

namespace ExemploDecorator
{
    public class RepositorioConfiguracaoImpl : IRepositorioConfiguracao
    {
        private readonly Dictionary<string, string> _configuracoes = new Dictionary<string, string>()
        {
            {"REGISTROS_POR_GRID", "50"},
            {"TEMPO_SESSAO", "20"},
            {"TIMEOUT_CONEXAO_DB", "30"}
        };

        public string ObterConfiguracao(string chave)
        {
            Thread.Sleep(3000);
            return _configuracoes[chave];
        }
    }
}


Vejam que esta classe implementa uma interface, um contrato que dita quais são os métodos e suas assinaturas obrigatórias. Este é um ponto muito importante do Decorator e não pode ser esquecido.

// IRepositorioConfiguracao.cs

namespace ExemploDecorator
{
    public interface IRepositorioConfiguracao
    {
        string ObterConfiguracao(string chave);
    }
}


Agora vamos pensar. Como seria o uso dessa classe que recupera configurações para o seu cliente? Em primeiro lugar, o seu cliente deve trabalhar referenciando a interface, e não a classe concreta. Este é outro ponto importante para minimizar o impacto de se incluir uma lógica de cache nesse nosso exemplo. Vamos ver o código que consome esse repositório.

// Program.cs
using System;
using System.Diagnostics;
using Ninject;

namespace ExemploDecorator
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Iniciando testes...\n");
            var kernel = new StandardKernel(new ModuloInjecaoDependencia());
            IRepositorioConfiguracao repositorio = kernel.Get<IRepositorioConfiguracao>();

            MostrarConfiguracao("TIMEOUT_CONEXAO_DB", repositorio);
            MostrarConfiguracao("TIMEOUT_CONEXAO_DB", repositorio);
            MostrarConfiguracao("REGISTROS_POR_GRID", repositorio);
            MostrarConfiguracao("REGISTROS_POR_GRID", repositorio);
            MostrarConfiguracao("REGISTROS_POR_GRID", repositorio);

            Console.WriteLine("FIM");
        }

        private static void MostrarConfiguracao(string chave, IRepositorioConfiguracao repositorio)
        {
            Console.WriteLine("Obtendo configuração para '{0}'...", chave);
            var cronometro = Stopwatch.StartNew();
            Console.WriteLine("Configuração = '{0}'.", repositorio.ObterConfiguracao(chave));
            cronometro.Stop();
            Console.WriteLine("Tempo da chamada : {0} ms. \n", cronometro.ElapsedMilliseconds);
        }
    }
}

// ModuloInjecaoDependencia.cs
using Ninject.Modules;

namespace ExemploDecorator
{
    public class ModuloInjecaoDependencia : NinjectModule
    {
        public override void Load()
        {
            Bind<IRepositorioConfiguracao>().To<RepositorioConfiguracaoImpl>();
        }
    }
}


Nosso programa está usando o Ninject para poder resolver a interface IRepositorioConfiguracao para RepositorioConfiguracaoImpl. Como não é objetivo deste post, deixo para vocês lerem a respeito deste ótimo framework para injeção de dependência. Outro detalhe que eu gostaria de comentar sobre este ponto é que eu poderia utilizar simplesmente uma classe factory para resolver a dependência, isolando os detalhes de como se cria a instância de IRepositorioConfiguracao do seu cliente. Escolhi o Ninject por pura vontade.

Executando este código, temos o seguinte resultado, onde cada chamada ao repositório leva em torno de 3 segundos.



Bom, se o cliente está trabalhando com a interface, então podemos trocar o uso da classe RepositorioConfiguracaoImpl por qualquer classe que implemente o contrato IRepositorioConfiguracao, correto? É aqui que vamos usar o Decorator. Vamos criar uma classe que implemente IRepositorioConfiguracao e que contenha, internamente, uma campo do tipo IRepositorioConfiguracao. Essa primeira classe, que chamaremos de RepositorioConfiguracaoComCache, irá receber uma instância de  RepositorioConfiguracaoImpl no seu construtor, que será armazenada internamente. Em toda a chamada do método ObterConfiguracao de RepositorioConfiguracaoComCache, ela irá fazer a lógica de controle de cache e, se necessário, redirecionar a chamada para o ObterConfiguracao de RepositorioConfiguracaoImpl para efetivamente recuperar o valor da configuração no "banco de dados" (ou seja, se a informação está no cache, usa o dado do cache; caso contrário, vai ao banco, pega o dado, atualiza o cache e retorna a informação).

// RepositorioConfiguracaoComCache.cs
using System.Runtime.Caching;

namespace ExemploDecorator
{
    public class RepositorioConfiguracaoComCache : IRepositorioConfiguracao
    {
        private ObjectCache _cache = MemoryCache.Default;
        private IRepositorioConfiguracao _repositorio;

        public RepositorioConfiguracaoComCache(IRepositorioConfiguracao repositorio)
        {
            _repositorio = repositorio;
        }

        public string ObterConfiguracao(string chave)
        {
            if (!_cache.Contains(chave))
            {
                string valor = _repositorio.ObterConfiguracao(chave);
                _cache.Add(chave, valor, null);
            }
            return (string)_cache[chave];
        }
    }
}


Obs. Estamos usando a classe ObjectCache, disponível a partir do .NET 4.0. Se você está criando este exemplo junto da leitura deste post, e criou um projeto do tipo ConsoleApplication, lembre-se de mudar a propriedade "Target framework" do seu projeto de ".NET Framework 4.0 Client profile" para ".NET Framework 4.0".

Para que o cliente passe a utilizar a nova classe RepositorioConfiguracaoComCache, basta alterar a configuração do Ninject no módulo que criamos.

// ModuloInjecaoDependencia.cs
using Ninject.Modules;

namespace ExemploDecorator
{
    public class ModuloInjecaoDependencia : NinjectModule
    {
        public override void Load()
        {
            Bind<IRepositorioConfiguracao>().ToMethod((ctx) => new RepositorioConfiguracaoComCache(new RepositorioConfiguracaoImpl()));
        }
    }
}


Com isso, ao executarmos novamente a aplicação, temos o acesso ao repositório feito com cache (primeiro acesso mais demorado, mas os demais rápidos)!


Bem útil, se bem aplicado, não acham?

Um último comentário. Esta não é a única maneira de se implementar esse tipo de funcionalidade. Existe um paradigma de programação chamado AOP (Aspect Oriented Programming) que trata da inclusão de comportamento adicional em classes já existentes, principalmente se o comportamento não for uma regra de negócio mas sim algo relacionado a log, segurança, etc. Sobre AOP, há alguns anos eu participei de um podcast sobre o assunto http://podcast.dotnetarchitects.net/2010/05/podcast-13programacao-orientada-a-aspecto/. Vale a pena conferir pois a técnica também é bem útil e em alguns casos mais fácil de implementar que o Decorator (principalmente para sistemas construídos sem levar em conta o uso correto da Orientação a Objetos).

[]'s

Comentários

Postagens mais visitadas deste blog

Trocando configurações padrão do Live TIM

Uma proposta de Clean Architecure com Modelo de Atores

Testes automatizados em sistemas autenticados com certificados digitais, usando Selenium e PhantomJS