[Delphi] Design Patterns GoF – Decorator

Boa noite, caros leitores!
Para sair um pouco da rotina, gostaria de convidá-los para aprender a trabalhar com decoração, haha!
Brincadeiras à parte, embora o artigo de hoje realmente não deixe de ser sobre decoração, apresentarei o padrão de projeto Decorator. Assim como o Bridge, um dos maiores propósitos deste padrão é reduzir as heranças de uma hierarquia de classes. Vamos decorar?

 

Como já sabemos, um dos pilares da programação orientada a objetos é a herança, que nos fornece um mecanismo para eliminação de código duplicado através do compartilhamento vertical de métodos e campos. Porém, sabe-se também que o excesso de heranças em uma hierarquia pode promover o aumento da complexidade e a dificuldade de manutenção do código. Uma pequena alteração no meio da hierarquia gera um impacto em todas as classes derivadas, trazendo, talvez, comportamentos inesperados.

O Design Pattern Decorator apresenta uma alternativa às heranças. Na verdade, a palavra “decorator” nos remete a um sentido de decoração que, em termos gerais, é o que realmente ocorre ao empregarmos este padrão de projeto.
O objetivo do Decorator, como veremos logo a seguir, é adicionar funcionalidades extras a um objeto em tempo de execução, de forma que ele possa ter novos comportamentos que originalmente não fazem parte de sua estrutura. Para os gamers de plantão, podemos dizer que a classe “sobe” de level e ganha novas habilidades, rsrs.
Na internet, é comum encontrar dois exemplos clássicos deste padrão: a janela e a forma geométrica. No primeiro, adiciona-se acessórios na janela, como cortinas, persianas ou grades. Já no outro, enfeita-se uma forma geométrica, desenhando bordas e preenchimentos. São exemplos que, apesar de ilustrativos, expressam o propósito do Decorator, porém, não refletem uma ideia de aplicação real.

Aqui no blog, apresentarei alguns cenários mais voltados à realidade.
Quando estudei os conceitos do Decorator, a princípio imaginei um contexto de um sistema de envio de mensagens. Inicialmente, teríamos um objeto de uma classe de mensagens e que, quando necessário, seria “decorado” por funcionalidades complementares, como assinatura, encriptação ou formatação HTML. Em tempo de execução, criaríamos o decorador de mensagens solicitado, como para a assinatura, e faríamos um vínculo com o objeto de mensagem. A partir deste momento no fluxo, o objeto ganharia, então, uma propriedade de assinatura.

 

Na teoria, o padrão Decorator apresenta 4 elementos:

  • Component: Interface comum que será implementada tanto pelas classes que poderão ser decoradas quanto pelas classes decoradoras;
  • Concrete Component: Classes que implementam Component e que apenas poderão receber “decorações”, ou melhor, receber funcionalidades extras;
  • Decorator: Classe abstrata que implementa Component e atua como classe base de todas as decorações possíveis;
  • Concrete Decorator: Classes que herdam de Decorator e exercem o papel de “decoradoras”.

No exemplo sobre o sistema de envio de mensagens, portanto, teríamos as seguintes interfaces e classes:

  • IMensagem: elemento Component;
  • TMensagem: elemento Concrete Component (implementa IMensagem);
  • TDecoradorMensagem: elemento Decorator (implementa IMensagem);
  • TAssinatura, TEncriptacao e TFormatacaoHTML (herdam de TDecoradorMensagem): Concrete Decorators.

 

Embora o exemplo sobre o sistema de mensagens seja bem compreensível, pensei que um cenário diferente para a nossa aplicação prática poderia agregar ainda mais. Sendo assim, implementaremos uma classe que exibe detalhes de exceções que ocorrem no sistema. A princípio, a classe apenas irá apenas exibir a mensagem de exceção.

 

Já sei! Estes três comportamentos serão heranças!
Não! Serão três agentes responsáveis por adicionar comportamentos adicionais a um objeto, ou, simbolicamente falando, três decorações.
O primeiro passo é criar a Interface Component. Ressalto aqui que, na maioria dos artigos, este sempre será o primeiro passo, já que devemos partir da abstração.

type
  ILogExcecao = interface
    function ObterDadosExcecao: string;
  end;

O método “ObterDadosExcecao” será responsável por devolver uma string com os detalhes da exceção solicitados.

O nosso Concrete Component, que implementa a Interface acima, terá a seguinte implementação:

type
  TLogExcecao = class(TInterfacedObject, ILogExcecao)
  private
    Excecao: Exception;
 
    function ObterDadosExcecao: string;
  public
    constructor Create(Excecao: Exception);
  end;
 
...
 
constructor TLogExcecao.Create(Excecao: Exception);
begin
  Self.Excecao := Excecao;
end;
 
function TLogExcecao.ObterDadosExcecao: string;
begin
  result := 'Mensagem: ' + Excecao.Message;
end;

Veja que a classe TLogExcecao, em seu estado primitivo, apenas retorna a mensagem de exceção, porém, na maioria das vezes, só isso não é o suficiente. Precisamos de mais detalhes para conseguirmos rastrear o erro. Partiremos, então, para a parte de “decoração”.

O próximo passo é criar o elemento Decorator, que será uma classe abstrata com uma característica muito importante: possui um campo que mantém uma referência ao objeto que será decorado:

type
  TDecorator = class(TInterfacedObject, ILogExcecao)
  protected
    LogExcecao: ILogExcecao;
 
    function ObterDadosExcecao: string; virtual;
  public
    constructor Create(LogExcecao: ILogExcecao);
  end;
 
...
 
constructor TDecorator.Create(LogExcecao: ILogExcecao);
begin
  // armazena uma referência para o objeto que será decorado
  Self.LogExcecao := LogExcecao;
end;
 
function TDecorator.ObterDadosExcecao: string;
begin
  result := LogExcecao.ObterDadosExcecao;
  result := result + #13#10;
end;

 

Por quê precisamos dessa referência?
Antes de adicionarmos as funcionalidades extras, devemos executar primeiro os comportamentos do objeto original. Em outras palavras, o método “ObterDadosExcecao” será executado para o objeto inicial e só depois para as decorações. Explico melhor nos parágrafos abaixo.

O último passo é finalmente criar as decorações. A primeira delas, que irá adicionar a informação de data e hora da exceção, terá a seguinte codificação:

type
  TDataHoraDecorator = class(TDecorator)
  protected
    function ObterDadosExcecao: string; override;
  end;
 
...
 
function TDataHoraDecorator.ObterDadosExcecao: string;
begin
  result := inherited ObterDadosExcecao;
  result := result + 'Data/Hora: ' + FormatDateTime('dd/mm/yyyy hh:nn:ss', Now);
end;

Observe que a variável “result”, em primeiro lugar, é preenchida com o resultado do método da classe ancestral que, por sua vez, executa o método do ConcreteComponent.
O decorador do nome do usuário terá uma codificação semelhante:

type
  TNomeUsuarioDecorator = class(TDecorator)
  private
    function ObterNomeUsuario: string;
  protected
    function ObterDadosExcecao: string; override;
  end;
 
...
 
function TNomeUsuarioDecorator.ObterDadosExcecao: string;
begin
  result := inherited ObterDadosExcecao;
  result := result + 'Usuário: ' + ObterNomeUsuario;
end;
 
function TNomeUsuarioDecorator.ObterNomeUsuario: string;
var
  Size: DWord;
begin
  // retorna o login do usuário do sistema operacional
  Size := 1024;
  SetLength(result, Size);
  GetUserName(PChar(result), Size);
  SetLength(result, Size - 1);
end;

Por fim, para a decoração da versão do Windows, acessaremos o registro do sistema operacional para obter os números da versão:

type
  TVersaoWindowsDecorator = class(TDecorator)
  private
    function ObterVersaoWindows: string;
  protected
    function ObterDadosExcecao: string; override;
  end;
 
...
 
function TVersaoWindowsDecorator.ObterDadosExcecao: string;
begin
  result := inherited ObterDadosExcecao;
  result := result + 'Versão do Sistema Operacional: ' + ObterVersaoWindows;
end;
 
function TVersaoWindowsDecorator.ObterVersaoWindows: string;
var
  Registro: TRegistry;
  MajorVersion: byte;
  MinorVersion: byte;
begin
  // No Windows 10, a aplicação deve ser executada como Administrador
  Registro := TRegistry.Create;
  try
    Registro.RootKey := HKEY_LOCAL_MACHINE;
    Registro.OpenKey('Software\Microsoft\Windows NT\CurrentVersion', False);
    MajorVersion := Registro.ReadInteger('CurrentMajorVersionNumber');
    MinorVersion := Registro.ReadInteger('CurrentMinorVersionNumber');
 
    case MajorVersion of
      5: case MinorVersion of
          1: result := 'Windows XP';
        end;
      6: case MinorVersion of
          0: result := 'Windows Vista';
          1: result := 'Windows 7';
          2: result := 'Windows 8';
          3: result := 'Windows 8.1';
        end;
      10: case MinorVersion of
          0: result := 'Windows 10';
        end;
    end;
  finally
    FreeAndNil(Registro);
  end;
end;

 

Perfeito! Agora é só colocar tudo isso em prática. Já digo de passagem: é bem simples!
Já que se trata de um gravador de log de exceções, achei oportuno utilizá-lo no evento OnException do componente TApplicationEvents, pois, neste ponto, temos acesso o objeto de exceção que vem por parâmetro (E). Neste evento, criamos uma instância da classe de gravação de log e, em seguida, aplicamos qualquer decoração que for necessária:

var
  LogExcecao: ILogExcecao;
begin
  LogExcecao := TLogExcecao.Create(E);
 
  // para "decorar" o objeto com data e hora da exceção
  if ExibirDataHora then
    LogExcecao := TDataHoraDecorator.Create(LogExcecao);
 
  // para "decorar" o objeto com o nome do usuário
  if ExibirNomeUsuario then
    LogExcecao := TNomeUsuarioDecorator.Create(LogExcecao);
 
  // para "decorar" o objeto com a versão do Windows
  if ExibirVersaoWindows then
    LogExcecao := TVersaoWindowsDecorator.Create(LogExcecao);
 
  // exibirá os campos conforme a decoração selecionada
  ShowMessage(LogExcecao.ObterDadosExcecao);
end;

 

Legal, não é? Mas podemos ainda ir além! No exemplo acima, há três condições IF que vinculam a decoração quando são atendidas, porém, é importante compreender que o objeto não se limita a somente um tipo de decoração. Podemos aplicar os 3 juntos! Neste caso, a mensagem de decoração, a data e hora, o nome do usuário e a versão do Windows serão exibidos de uma vez só. Vale explicar que, para que isso seja possível, uma decoração deve “decorar a outra”, como exemplificado abaixo:

var
  LogExcecao: ILogExcecao;
begin
  LogExcecao := TLogExcecao.Create(E);
 
  // o objeto é decorado com a data e hora
  LogExcecao := TDataHoraDecorator.Create(LogExcecao);
 
  // o objeto já decorado recebe uma nova decoração do nome do usuário
  LogExcecao := TNomeUsuarioDecorator.Create(LogExcecao);
 
  // o objeto já decorado duas vezes recebe uma terceira decoração
  LogExcecao := TVersaoWindowsDecorator.Create(LogExcecao);
 
  // executa as três decorações em conjunto
  ShowMessage(LogExcecao.ObterDadosExcecao);
end;

 

No more inheritances! 🙂
Eu diria que, neste contexto do Decorator, acontece, sim, uma espécie de herança, mas de forma horizontal.
Uma vantagem do Decorator que não posso deixar de citar é a possibilidade de decorar qualquer outro objeto do sistema, claro, desde que ele seja do tipo de uma classe que implementa a Interface Component. Por exemplo, podemos aplicar essas mesmas decorações para uma classe de auditoria ou até mesmo em uma classe de mensagem de e-mail, como mencionado no início do artigo. Herança? Não precisa!

Bom, pessoal, antes de finalizar, gostaria de mencionar um fato. Enquanto eu procurava um contexto para apresentar o exemplo prático do artigo, a minha esposa (também conhecida como Soberana do JMeter!) deu uma ótima sugestão de um sistema de pagamentos que se encaixaria perfeitamente no artigo. Neste cenário, teríamos um tipo de pagamento que poderia ser “decorado” de três formas: por boleto, depósito ou cartão de crédito. Para cada forma, o programa criaria, em tempo de execução, os campos para digitação, como código de barras, agência/banco e número do cartão, respectivamente. Infelizmente não pude utilizá-lo em função da quantidade de número de linhas de código, pois deixaria o artigo muito extenso.

Assim como todos os outros artigos, disponibilizei um projeto de exemplo no link abaixo. Faça o download, execute-o no Delphi e confira a decoração do objeto conforme a seleção dos CheckBoxes na tela. Para testar melhor o exemplo (sem que o Delphi interrompa a execução a cada exceção), desmarque a opção “Stop on Delphi Exceptions” nas opções da IDE.

Exemplo de Decorator com Delphi

 

Estou à disposição para esclarecer qualquer dúvida, leitores!
Um grande abraço a todos e até o próximo padrão de projeto!


 

Compartilhe!
Share on FacebookTweet about this on TwitterShare on LinkedInShare on Google+Pin on PinterestEmail this to someone

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.