Hello, leitores!
Hoje finalmente encerraremos a série sobre Design Patterns!
Dessa vez, estudaremos o Visitor. Embora apresente uma proposta interessante, o conceito desse padrão de projeto, a princÃpio, pode parecer um pouco confuso. Mesmo assim, farei o possÃvel para explicá-lo com detalhes. Vamos nessa!
Introdução
Quando trabalhamos com regras de negócio complexas, que exigem a criação e manipulação de um conjunto de objetos, há uma tendência de crescimento da quantidade de classes ao longo do tempo, dificultando a manutenção da arquitetura. O contexto fica ainda mais crÃtico quando é necessário executar diferentes operações em cada um dos objetos existentes. No final das contas, há vários objetos criados e várias operações disponÃveis, e o “cruzamento” entre eles é o que induz o aumento da complexidade técnica no projeto.
Como exemplo, imagine uma aplicação que tenha algumas classes de modelagem (também chamadas de classes de estrutura neste contexto), como clientes, fornecedores e produtos. Considere também que essa aplicação traz uma funcionalidade para exportar dados em formatos CSV, JSON e XML. Cada classe de modelagem, portanto, deve disponibilizar três métodos responsáveis por tratar a exportação para cada formato de arquivo.
Parece algo trivial, porém, imagine que um novo formato de exportação surja na regra de negócio e que novos campos sejam adicionados às classes de estrutura. Além de adicionar novos métodos, será necessário também alterar os métodos de exportação em cada uma delas. Já começa a ficar um pouco custoso, não é?
Essa alteração constante nas classes de estrutura fere um princÃpio da Engenharia de Software conhecido como Open/Closed Principle, ou OCP, no qual declara que classes devem ser abertas para extensão e fechadas para modificação, justamente para minimizar o impacto magnético na arquitetura.
A proposta do Visitor é simplificar cenários com essa caracterÃstica ao separar as estruturas das operações. O padrão de projeto orienta a criação de classes de estruturas e classes de operações de forma isolada. Com isso, novas operações podem ser adicionadas sem promover impactos nas estruturas.
O Visitor foi concebido com este nome por fazer uma analogia à “visitas”, já que os objetos são “visitados” pelas operações. Em cada visita, o objeto pode sofrer alterações internas, recebendo novos valores, ou processar dados, gerando alguma saÃda. De qualquer forma, tudo ocorre fora da classe de estrutura.
Arquitetura
No contexto do Visitor, as classes de estrutura são definidas como Element. Os “visitantes”, que executam as operações, são chamados de Visitor (claro!). Essas duas unidades são representadas como Interfaces na arquitetura. As classes que as implementam, portanto, recebem o nome de Concrete Element e Concrete Visitor, respectivamente.
O último participante é o Object Structure. Trata-se de um conjunto de objetos da classe Concrete Element no qual os visitantes executarão suas operações. Em termos técnicos, o Object Structure é uma lista que armazena objetos da(s) classe(s) Concrete Element.
O detalhe mais relevante neste padrão de projeto ocorre na Interface Visitor. Nela, há um método chamado Visit
para cada Concrete Element existente. No exemplo acima, sobre as importações, terÃamos três métodos Visit
para corresponder com a quantidade de classes de estrutura: clientes, fornecedores e produtos.
No Delphi, podemos declarar mais de um método com o mesmo nome desde que utilizemos a palavra overload
para ativar a sobrecarga de métodos proporcionada pela orientação a objetos. Na prática, terÃamos as seguintes declarações:
1 2 3 |
procedure Visit(Cliente: TCliente); overload; procedure Visit(Fornecedor: TFornecedor); overload; procedure Visit(Parceiro: TProduto); overload; |
A Interface Element, por sua vez, declara um método chamado Accept
, que recebe um Visitor como parâmetro para executar o método Visit
. É neste ponto que a sobrecarga entra em ação. O próprio Visitor identificará o método sobrecarregado a ser executado de acordo com o Element que o chamou. Vejam só:
1 2 3 4 |
procedure TCliente.Accept(Visitor: IInterface); begin Visitor.Visit(Self); end; |
O Self
, neste momento, é um objeto da classe TCliente
, logo, o Visitor executará a primeira versão do método Visit
automaticamente.
Exemplo de codificação do Visitor
Tudo ainda parece um pouco confuso, não é?
Para solidificar este conceito, utilizaremos o Visitor em um módulo de RH que calcula o aumento de salários e a definição de senioridade dos funcionários de uma empresa. Para que o exemplo não fique extenso, teremos apenas dois tipos de funcionários (programador e gerente), e dois tipos de operações (cálculo de salário e identificação de senioridade).
Interface Element
A primeira etapa é codificar a Interface Element. No entanto, neste momento, já enfrentamos um impasse: o Element usa o Visitor e o Visitor usa o Element. Se codificarmos dessa forma no Delphi, receberemos um erro de compilação criticando a referência circular entre units. Para evitar essa restrição, usaremos o tipo primitivo IInterface
na unit do Element. Posteriormente, na implementação do método, faremos um typecast para o tipo do Visitor:
1 2 3 4 5 |
type IElement = interface // Método que chamará o Visitor para executar a operação procedure Accept(Visitor: IInterface); end; |
Classe Concrete Element
Prosseguindo, codificaremos as classes Concrete Element referente aos tipos de funcionário “programador” e “gerente”. Neste caso, como eles possuem propriedades em comum, julgo mais adequado criar uma classe base chamada TFuncionario
para evitar a duplicação de código:
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 |
uses Pattern.Element; type { Concrete Element } TFuncionario = class(TInterfacedObject, IElement) private FNome: string; FFuncao: string; FAdmissao: TDateTime; FSalario: real; FSenioridade: string; public property Nome: string read FNome write FNome; property Funcao: string read FFuncao write FFuncao; property Admissao: TDateTime read FAdmissao write FAdmissao; property Salario: real read FSalario write FSalario; property Senioridade: string read FSenioridade write FSenioridade; // Método que será sobrescrito pelas subclasses (Concrete Elements) procedure Accept(Visitor: IInterface); virtual; end; implementation { TFuncionario } procedure TFuncionario.Accept(Visitor: IInterface); begin Exit; end; |
Observe que o método Accept
é virtual
, indicando que terá de ser implementado nas subclasses.
Para o perfil “Programador”, teremos a codificação abaixo. Atente-se aos comentários do método Accept
:
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 |
uses Pattern.ConcreteElement; type TProgramador = class(TFuncionario) public procedure Accept(Visitor: IInterface); override; end; implementation uses System.SysUtils, Pattern.Visitor; { TProgramador } procedure TProgramador.Accept(Visitor: IInterface); var ConcreteVisitor: IVisitor; begin // Aplica um typecast do parâmetro para o tipo IVisitor ConcreteVisitor := Visitor as IVisitor; // Chama o método "Visit" do Concrete Visitor, enviando a própria instância como parâmetro. // Essa instância é o que indicará qual sobrecarga do método "Visit" será chamado. ConcreteVisitor.Visit(Self); end; |
Em breve, quando codificarmos os Visitors, tudo ficará mais claro.
A classe referente ao gerente segue o mesmo padrã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 |
uses Pattern.ConcreteElement; type TGerente = class(TFuncionario) public procedure Accept(Visitor: IInterface); override; end; implementation uses System.SysUtils, Pattern.Visitor; { TGerente } procedure TGerente.Accept(Visitor: IInterface); var ConcreteVisitor: IVisitor; begin // Aplica um typecast do parâmetro para o tipo IVisitor ConcreteVisitor := Visitor as IVisitor; // Chama o método "Visit" do Concrete Visitor, enviando a própria instância como parâmetro. // Essa instância é o que indicará qual sobrecarga do método "Visit" será chamado. ConcreteVisitor.Visit(Self); end; |
Notei que a implementação do método “Accept” é idêntico para as duas classes. Não faria mais sentido movê-la para a classe base?
Faria todo sentido, porém, lembre-se de que precisamos informar a própria instância do chamador para que a sobrecarga funcione. Sendo assim, como não teremos uma sobrecarga do método Visit
para a classe TFuncionario
– já que é uma classe base – o correto é manter as implementações apenas para TProgramador
e TGerente
. Essa separação também é útil para situações em que a chamada ao Visitor deve ser precedida de ações distintas entre as classes.
Interface Visitor
Acompanhem, agora, a codificação da Interface Visitor:
1 2 3 4 5 6 7 8 9 10 11 12 |
uses Pattern.ConcreteElementProgramador, Pattern.ConcreteElementGerente; type IVisitor = interface ['{9030B2DC-C821-4C91-861C-9322D2C04EA3}'] // O Visitor possui um método sobrecarregado para cada Concrete Element existente procedure Visit(Programador: TProgramador); overload; procedure Visit(Gerente: TGerente); overload; end; |
Classes Concrete Visitors
Há uma sobrecarga para cada tipo de funcionário. As classes Concrete Visitor, a seguir, deverão obedecer essa sobrecarga, executando as regras de negócio especificadas para cada tipo.
O cálculo do aumento do salário é responsabilidade do primeiro Concrete Visitor. Procurei adicionar vários comentários para aprimorar a compreensã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 49 50 51 |
uses Pattern.Visitor, Pattern.ConcreteElementProgramador, Pattern.ConcreteElementGerente; type TSalario = class(TInterfacedObject, IVisitor) // Método que será invocado quando o objeto do parâmetro for da classe TProgramador procedure Visit(Programador: TProgramador); overload; // Método que será invocado quando o objeto do parâmetro for da classe TGerente procedure Visit(Gerente: TGerente); overload; end; implementation uses System.SysUtils, DateUtils; { TSalario } // Cálculo do aumento do salário para programadores procedure TSalario.Visit(Programador: TProgramador); var PorcentagemPorDiaTrabalhado: real; begin // Aplica um aumento de 6% no salário Programador.Salario := Programador.Salario * 1.06; // Aplica um aumento adicional de 0,002% por cada dia trabalhado PorcentagemPorDiaTrabalhado := DaysBetween(Date, Programador.Admissao) * 0.002; Programador.Salario := Programador.Salario * (1 + PorcentagemPorDiaTrabalhado / 100); end; // Cálculo do aumento do salário para gerentes procedure TSalario.Visit(Gerente: TGerente); var QtdeAnosNaEmpresa: byte; begin // Aplica um aumento de 8% no salário Gerente.Salario := Gerente.Salario * 1.08; // Calcula a quantidade de anos que o gerente está na empresa QtdeAnosNaEmpresa := YearsBetween(Date, Gerente.Admissao); // Conforme a quantidade de anos, uma porcentagem adicional é aplicada case QtdeAnosNaEmpresa of 2..3: Gerente.Salario := Gerente.Salario * 1.03; // até 3 anos: 3% 4..5: Gerente.Salario := Gerente.Salario * 1.04; // até 5 anos: 4% 6..10: Gerente.Salario := Gerente.Salario * 1.05; // até 10 anos: 5% end; end; |
O segundo Concrete Visitor destina-se a identificar o nÃvel de senioridade do funcionário. O tempo de casa e as descrições dos nÃveis também são distintas para programadores e gerentes:
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 |
uses Pattern.Visitor, Pattern.ConcreteElementProgramador, Pattern.ConcreteElementGerente; type { Concrete Visitor } TSenioridade = class(TInterfacedObject, IVisitor) // Método que será invocado quando o objeto do parâmetro for da classe TProgramador procedure Visit(Programador: TProgramador); overload; // Método que será invocado quando o objeto do parâmetro for da classe TGerente procedure Visit(Gerente: TGerente); overload; end; implementation uses System.SysUtils, DateUtils; { TSenioridade } // Definição da senioridade para programadores procedure TSenioridade.Visit(Programador: TProgramador); begin // Define a senioridade do programador conforme o tempo de casa case YearsBetween(Date, Programador.Admissao) of 0..1: Programador.Senioridade := 'Júnior'; 2..3: Programador.Senioridade := 'Pleno'; 4..5: Programador.Senioridade := 'Sênior'; 6..8: Programador.Senioridade := 'Especialista'; end; end; // Definição da senioridade para gerentes procedure TSenioridade.Visit(Gerente: TGerente); begin // Define a senioridade do gerente conforme o tempo de casa case YearsBetween(Date, Gerente.Admissao) of 0..2: Gerente.Senioridade := 'Qualificado'; 3..5: Gerente.Senioridade := 'Profissional'; 6..8: Gerente.Senioridade := 'Experiente'; end; end; |
Bom, pessoal, a implementação já está quase concluÃda.
Agora trabalharemos no Client, que consumirá as classes acima. O protótipo de formulário abaixo foi elaborado para simular a inclusão de funcionários e a execução das operações dos Visitors:
Lembram-se que comentei sobre o Object Structure, que armazena uma lista de objetos do tipo Concrete Element? Para utilizá-lo em nosso código, podemos declará-lo com o tipo TObjectList<T>
nativo do Delphi, dispensando a criação de mais uma classe na arquitetura.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
type TForm1 = class(TForm) { ... } private FObjectStructure: TObjectList<TFuncionario>; end; { ... } procedure TForm1.FormCreate(Sender: TObject); begin // Cria a instância do Object Structure FObjectStructure := TObjectList<TFuncionario>.Create; end; |
Para adicionar um novo objeto à essa lista, basta criar e preencher um objeto da classe Concrete Element:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var Element: TFuncionario; begin Element := nil; // Cria o Concrete Element (Programador ou Gerente) conforme seleção na TComboBox case ComboBoxFuncao.ItemIndex of 0: Element := TProgramador.Create; 1: Element := TGerente.Create; end; // Preenche os dados do objeto Element.Nome := EditNome.Text; Element.Funcao := ComboBoxFuncao.Text; Element.Admissao := DateTimePickerAdmissao.Date; Element.Salario := StrToFloatDef(EditSalario.Text, 0); // Adiciona na Object Structure (lista de objetos) FObjectStructure.Add(Element); end; |
Para demonstração da funcionalidade, adicionei alguns dados de exemplo. Provavelmente você já conhece um destes funcionários:
Em ação!
A última etapa é colocar os Visitors para funcionar!
O código do botão “Calcular Novos Salários” criará uma instância do Concrete Visitor TSalario
e percorrerá o Object Structure, chamando o método Accept
 de cada item indicando o Visitor como parâmetro:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var Visitor: IVisitor; Element: TFuncionario; begin // Cria uma instância do Concrete Visitor referente ao aumento de salário Visitor := TSalario.Create; // Chama o método Accept para executar a operação em cada elemento da Object Structure for Element in FObjectStructure do begin Element.Accept(Visitor); end; end; |
O evento para definir o nÃvel de senioridade, por sua vez, apresenta a mesma codificação, exceto o Concrete Visitor instanciado que, neste caso, é TSenioridade
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var Visitor: IVisitor; Element: TFuncionario; begin // Cria uma instância do Concrete Visitor referente à definição da senioridade Visitor := TSenioridade.Create; // Chama o método Accept para executar a operação em cada elemento da Object Structure for Element in FObjectStructure do begin Element.Accept(Visitor); end; end; |
Este é o resultado:
Sensacional, não? 🙂
Conclusão
Ao observar a simplicidade do código no Client, fica fácil identificar as vantagens da utilização do padrão de projeto, sem dizer, claro, que a manutenção torna-se muito simples. Mesmo que novas operações sejam adicionadas, garantimos que os comportamentos existentes nas classes de estrutura não sejam afetados.
Por exemplo, suponha que seja necessário calcular atualizações das cestas de benefÃcios dos funcionários. Neste caso, somente os Visitors serão alterados. As classes de estrutura, ou melhor, os Concrete Elements, permanecem estáveis.
Da mesma forma, caso um novo tipo de funcionário seja incluÃdo, como TDiretor
, apenas os Visitors receberão alterações, que se resumem na criação de mais um método Visit
sobrecarregado. Só isso.
Pensando assim, podemos afirmar que, quando chamamos algum método informando Self
como parâmetro, estamos basicamente reproduzindo uma “pequena versão” do método Visit
. =D
E só pra fechar, você deve ter notado que exibo uma lista de objetos em uma TStringGrid
. Para isso, usei LiveBindings, meu caro! Aprendi essa ótima dica com um desenvolvedor chamado Lucas Chagas. Conforme a sua orientação, podemos utilizar o componente TAdapterBindSource
para vincular uma lista de objetos a um controle visual. Além disso, todas as alterações efetuadas nos objetos da lista são automaticamente refletidas no componente TStringGrid
. Muito bom, hein? Obrigado, Lucas!
O projeto de exemplo deste artigo, com esse mecanismo do LiveBindings, está disponÃvel no link abaixo:
Bom, leitores, missão cumprida!
Agradeço fortemente por terem acompanhado toda essa série de artigos sobre Design Patterns.
Dentro de alguns dias, publicarei uma retrospectiva dessa série com links e breves descrições.
Um grande abraço!
Muito show André, o que vai vir de artigos agora com o fim deste?
Olá, Ricardo, obrigado!
Nessa série de artigos, abordei os padrões de projeto do Gang of Four (GoF).
A intenção agora é continuar apresentando outros padrões de projetos, como o GRASP.
Continue acompanhando!
Abraço!
Grande André!
Excelente essa série sobre design patterns com Delphi. Me agregou muito mesmo.
Minha sugestão para os próximos artigos seria criar uma nova série com dicas e macetes para quem está pensando em obter a certificação Delphi. =)
Forte abraço e continue com o seu ótimo trabalho!
Boa sugestão, Cleo!
Vou tentar preparar um material bem produtivo sobre os tópicos abordados na certificação.
Enquanto isso, continue acompanhando que já já entro nos padrões GRASP.
Obrigado, meu caro!
Abraço!