[Delphi] Design Patterns GoF – Visitor

[Delphi] Design Patterns - Visitor

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!

 

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.

 

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.

 

Mas, no Delphi, não conseguimos declarar mais de um método com o mesmo nome…
Podemos, sim, desde que utilizemos a palavra overload na frente do nome do método para ativar a sobrecarga de métodos proporcionada pela orientação a objetos. Na prática, teríamos as seguintes declarações:

procedure Visit(Cliente: TCliente); overload;
procedure Visit(Fornecedor: TFornecedor); overload;
procedure Visit(Parceiro: TProduto); overload;

 

E quem chama este método?
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ó:

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.

 

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). 

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:

type
  IElement = interface
    // Método que chamará o Visitor para executar a operação
    procedure Accept(Visitor: IInterface);
  end;

 

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:

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 programador, teremos a codificação abaixo. Atente-se aos comentários do método Accept:

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:

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.

 

Acompanhem, agora, a codificação da Interface Visitor:

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;

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:

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:

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:

Exemplo de Formulário para Cadastro de Funcionários

 

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.

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:

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:

Exemplos de dados de funcionários para demonstraçã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:

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:

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:

Exemplo da utilização do padrão de projeto Visitor

 

Sensacional, nã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

 

Entendido! Mas ainda fiquei com uma dúvida: você apresenta uma lista de objetos em uma TStringGrid? Como?
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:

Exemplo de Visitor com Delphi

Ou, se você preferir, no GitHub:

https://github.com/AndreLuisCelestino/Delphi-DesignPatterns/tree/master/Visitor-AndreCelestino

 

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!


 

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

4 comentários

    1. 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!

  1. 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!

    1. 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!

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.