[Delphi] Design Patterns GoF – 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!

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:

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ó:

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:

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:

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:

Em breve, quando codificarmos os Visitors, tudo ficará mais claro.

A classe referente ao gerente segue o mesmo padrão:

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:

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:

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:

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.

Para adicionar um novo objeto à essa lista, basta criar e preencher um objeto da classe Concrete Element:

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

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:

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:

Este é o resultado:

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

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!


 

André Celestino