C# -  O padrão Comportamental Gof - Visitor


 Neste artigo vou apresentar o padrão comportamental Gof Visitor.

Segundo a definição da Gang Of Four(GoF), o padrão Visitor representa uma operação a ser realizada sobre os elementos da estrutura de um objeto.

Este padrão permite que criar uma nova operação sem precisar mudar a classe dos elementos sobre as quais ela opera, e, isto é  uma forma de separar um algoritmo da estrutura de um objeto. O padrão permite também adicionar novos comportamentos à hierarquia de classes existente sem alterar nenhum código existente.

Desta forma o padrão Visitor separa os comportamentos não relacionados do objeto e os coloca em um objeto separado
criando um objeto separado para cada nova funcionalidade.

O uso deste padrão é recomendado quando você tem operações distintas e não relacionadas para executar em uma estrutura de objetos. A sua utilização evita adicionar código em toda a estrutura do objeto.

O padrão Visitor não é um padrão muito comum devido à sua complexidade e aplicabilidade limitada.



Este padrão é aderente aos seguintes princípios SOLID :

1- Princípio da Responsabilidade Única (SRP)
2- Princípio Aberto/Fechado (OCP)


Na sua implementação o seu objeto pode delegar responsabilidades a diferentes objetos.  Por Exemplo:

- Você tem um objeto Funcionario responsável pelas operações CRUD;
- Mas precisa adicionar outra responsabilidade ao objeto Funcionario como gerar relatórios;

Usando o padrão Visitor você pode adicionar comportamentos para gerar relatórios sem modificar o objeto Funcionario.

Basta criar uma interface Visitor e definir a nova funcionalidade que deseja implementar na classe Funcionario. Assim o seu objeto Funcionario segue o princípio Aberto/Fechado,  pois esta incluindo um novo comportamento sem precisar alterar o objeto.

Visitor : Exemplo

Vamos entender a atuação deste padrão com um exemplo.

- Você possui um tipo de estrutura de dados que consiste de um  grande número de nós:


Na figura temos a representação simplificada de 4 nós de elementos do tipo X.

O despacho duplo é um recurso que você pode usar na linguagem C# para controlar como a comunicação flui entre dois objetos.  Um uso frequente do recurso é passar o "this" para uma função em outra classe, permitindo que essa classe se comunique de volta ou manipule a instância do objeto de chamada.

Então como contornar esse problema ?

O padrão Visitor fornece uma solução para este cenário e implementa o despacho duplo. A solução proposta é aplicar o padrão Visitor onde a usamos uma classe especial chamada Visitor que pode realizar uma operação em um único nó.



- A classe tem um método Visit que recebe um nó como um argumento e realizar a operação requerida no nó;
- Existe um subclasse Visitor para cada operação a ser realizada;
- Existe também uma estrutura de dados complexa consistindo de um ou mais nós;
- Cada nó é chamado de um Element e possui um único método chamado Accept que é fornecido para o visitor;
- O método Accept recebe o visitor como um argumento e chama o seu método Visit com este ponteiro como um argumento;

Temos aqui representados:

1º Despacho – Client chama o método virtual Element.Accept
2º Despacho –
Element chama o método polimórfico Visitor.Visit


O objeto Visitor irá visitar cada elemento da estrutura do objeto e realizar a operação comum. (É possível variar a operação conforme a variação do Visitor)

Diagrama UML

O diagrama UML do padrão Visitor segundo o Gof apresenta os seguintes participantes

1- Visitor -  Declara uma operação Visit para cada classe de ConcreteElement na estrutura do objeto. O nome e a assinatura da operação identificam a classe que envia a solicitação Visit ao Visitor.

Isso permite ao visitante determinar a classe concreta do elemento que está sendo visitado. Em seguida, o visitante pode acessar os elementos diretamente por meio de sua interface específica

2- ConcreteVisitor - Implementa cada operação declarada pelo Visitor.  Cada operação implementa um fragmento do algoritmo definido para a classe ou objeto correspondente na estrutura.

3- Element - Define uma operação Accept que toma um Visitor como argumento.

4- ConcreteElement -  Implementa uma operação Accept que leva um Visitor como argumento.

5- ObjectStructure -  - Pode enumerar seus elementos e fornece uma interface permitindo que Visitor visite seus elementos.

Quando podemos usar o padrão

Podemos usar o padrão Visitor nos seguintes cenários :

- Quando uma estrutura de objeto tem muitas operações não relacionadas para ser executar.
- Quando a estrutura de um objeto não pode ser alterada, mas você precisa realizar novas operações nela.
- As operações precisam ser executadas nas classes concretas de uma estrutura de objeto.
-Quando as operações devem ser capazes de operar em múltiplas estruturas de objetos que implementam a mesma interface.

Vantagens do padrão

Como vantagens deste padrão temos que :

- Adicionar uma ação a todos os objetos Element de um estrutura é fácil, pois você só precisa implementar a interface Visitor. Não há necessidade de modificar todos os objetos Element para adicionar uma ação.

- Permite reagrupar ações comuns a muitos objetos Element em uma única classe de Visitor. Apenas o código para essa ação está nessa classe Visitor.

Desvantagem

E como desvantagem podemos citar que :

O código dos objetos Element estão espalhados em todos os objetos Visitor. Portanto, a lógica do objeto Element vive em muitas classes. Isso torna o código mais difícil de ler se você quiser examinar o código de um objeto Element;

- É necessário uma nova classe Visitor para cada ação;

Aplicação prática do padrão

Vamos supor que temos uma concessionária que possui uma coleção de veículos onde temos definido, o nome, o modelo e o preço. Assim podemos obter o preço normal do veiculo usando a classe Carro.

Agora precisamos aplicar um desconto promocional ao preço dos carros, mas queremos fazer isso sem ter que alterar a classe Carro

Para isso vamos implementar o padrão Visitor implementando um PrecoVisitor  que vai visitar o preço do carro e vai calcular e retornar o preço do veiculo com desconto.  Assim vamos incluir uma nova operação na classe sem alterá-la
segundo o principio aberto fechado.

Implementação prática

Levando em conta este cenário vamos implementar o padrão Visitor usando uma aplicação Console .NET Core (.NET 5.0) criada no VS 2019 Community.

A seguir temos o diagrama de classes obtido a partir do VS 2019 na implementação do padrão:

Aqui fizemos o seguinte:

  1. Criamos duas interfaces : ILoja e IVisitor.
  2. A interface ILoja tem apenas um único método Visit que leva o parâmetro da interface IVisitor.
  3. A interface IVisitor possui métodos Accept para cada classe que implementa a interface ILoja.
  4. Cada classe derivada de ILoja implementa o método Visit (IVisitor visitor).
  5. A implementação do método Visit chama o método visitor.Accept usando o parâmetro this.
  6. Se Carro chamar o método Accept, então no método Accept de PrecoVisitor é chamado e no método Accept podemos fazer nosso processamento no objeto Carro.

A seguir temos o código usado na implementação:

1- A interface IVisitor

    //Visitor
    public interface IVisitor
    {
        void Accept(Carro carro);
    }

2- A Interface ILoja

    //Element
    public interface ILoja
    {
        void Visit(IVisitor visitor);
    }
 

3- Classe PrecoVisitor

using System;
namespace ExemploVisitor1
{
    //ConcreteVisitor
    public class PrecoVisitor : IVisitor
    {
        private const int CARRO_DESCONTO =12;
        public void Accept(Carro carro)
        {
            decimal precoCarroAposDesconto = carro.Preco 
                - ((carro.Preco / 100) * CARRO_DESCONTO);
            Console.WriteLine($"{carro.Modelo} {carro.Nome} :" +
                $" ${precoCarroAposDesconto} ");
        }
    }
}

4- Classe Carro

   //ConcreteElement
    public class Carro : ILoja
    {
        public string Nome { get; set; }
        public decimal Preco { get; set; }
        public string Modelo { get; set; }
        public void Visit(IVisitor visitor)
        {
            visitor.Accept(this);
        }
    }

7- Program

using System;
using System.Collections.Generic;

namespace ExemploVisitor1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("### Padrão Visitor ###\n");

            List<Carro> carros = new List<Carro>();
            carros.Add(new Carro() { Nome = "A1", Preco = 2000M, Modelo = "Mercedes" });
            carros.Add(new Carro() { Nome = "458", Preco = 3500M, Modelo = "Ferrari" });
            carros.Add(new Carro() { Nome = "718 GTS", Preco = 2800M, Modelo = "Porsche" });

            List<ILoja> lojas = new List<ILoja>();

            Console.WriteLine("Preços normais dos carros\n");
            foreach (var carro in carros)
            {
                Console.WriteLine($"  {carro.Modelo} {carro.Nome} :  ${carro.Preco}");
                lojas.Add(carro);
            }

            Console.WriteLine("\nPreços dos carros com desconto");
            Console.WriteLine("Aplicando o padrão Visitor");
            Console.WriteLine("tecle algo...");

            Console.ReadLine();
            Console.WriteLine("Novos Preços com desconto de 12%\n");

            //Exibe o preço de cada item usando o Visitor
            PrecoVisitor precoVisitor = new PrecoVisitor();
            foreach (var element in lojas)
            {
                element.Visit(precoVisitor);
            }

            Console.ReadLine();
        }
    }
}

A execução do projeto irá apresentar o seguinte resultado:

Temos assim um exemplo básico de implementação do padrão Visitor.

Este padrão pode ser usado junto com o padrão Iterator para percorrer uma estrutura de dados complexas e executar alguma operação sobre os seus elementos.

Pegue o código do projeto aqui :   ExemploVisitor1.zip

"Deus tenha misericórdia de nós e nos abençoe; e faça resplandecer o seu rosto sobre nós (Selá.)
Para que se conheça na terra o teu caminho, e entre todas as nações a tua salvação."
Salmos 67:1,2

Referências:


José Carlos Macoratti