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?
Introdução
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 em uma parte da hierarquia pode gerar 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.
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.
Exemplo de codificação do Decorator
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 (implementaIMensagem
);TDecoradorMensagem
: elemento Decorator (implementaIMensagem
);TAssinatura
,TEncriptacao
eTFormatacaoHTML
(herdam deTDecoradorMensagem
): 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, e iremos decorá-la para exibir informações adicionais.
Interface Component
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.
1 2 3 4 |
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.
Classe Concrete Component
O nosso Concrete Component, que implementa a Interface acima, terá a seguinte implementação:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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”.
Classe Decorator
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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; |
Explico o motivo da 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. Continue acompanhando o artigo.
Classes Concrete Decorators
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
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!
Em ação!
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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! 🙂
Conclusão
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”) sugeriu 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.
Estou à disposição para esclarecer qualquer dúvida, leitores!
Um grande abraço a todos e até o próximo padrão de projeto!
Boa noite,
Gostaria de parabenizar pelo ótimo trabalho feito.
E tambem que o código disponibilizado está com um erro na linha 41 da unit Patter.Decorator.VersaoWindows
MajorVersion := Registro.ReadInteger(‘CurrentMajorVersionNumber’);
MinorVersion := Registro.ReadInteger(‘CurrentMinorVersionNumber’);
Estou usando windows 7.
Abraço.
Olá, Alexandre, tudo bem?
A função que busca a versão do Windows acessa o registro, portanto é possÃvel que ocorra alguns erros relacionados à permissão, mesmo no Windows 7.
Neste caso, experimente executar a aplicação com privilégios de administrador.
Abraço!
Parabéns pelo post André, no entanto tenho uma dúvida: ao fazer isso as classes Concrete Decorator nunca poderão ser acessadas diretamente sem antes ter uma instância da classe TLogExcecao(Concrete Component), é isso mesmo?
Boa noite, Joao! Isso mesmo.
O propósito das classes Concrete Decorator é exclusivamente decorar o objeto
TLogExcecao
.Porém, observe que, no artigo, as classes são “forçadas” a receber um objeto que implementa
ILogExcecao
. Em um cenário real, os Decorators podem ser mais genéricos, recebendo qualquer objeto (de uma Interface ou herança especÃfica) para ser decorado.Abraço!