Ir ao conteúdo

Padrão de Projeto Visitor em PHP com exemplo

Atualizado pela última vez em 9 de dezembro de 2021

O Padrão Visitor tem sua intenção bem objetiva e clara. Ele representa uma operação a ser executada nos elementos de uma estrutura de objetos. Ou seja, ele consegue definir uma nova operação em um determinado elemento sem mudar as classes sobre as quais esses elementos operam.

Diagrama UML para representar o Padrão de Projeto Visitor
Diagrama UML para representar o Padrão de Projeto Visitor

Quando utilizar este padrão?

Imagine que você tem uma estrutura de objetos formada por classes que estão interconectadas em uma estrutura similar a um grafo, com nodes e arestas. Suponha que você queira adicionar uma nova funcionalidade a estas classes, como você pode agir? Sem pensar racionalmente, você já poderia falar: “- vou editar a classe existente e adicionar a funcionalidade a ela”. Só que você estaria ferindo o Open Closed Principle (OCP).

Suponha agora que você tenha feito mesmo assim essa implementação e agora você precisaria adicionar mais um novo comportamento. Já dá para ver que não é uma solução elegante. Além disso, você cometeria o risco de ter efeitos colaterais em sua arquitetura.

Pensando sobre estas questões, o padrão Visitor sugere que coloquemos uma classe separada para “visitar” um classe já existente e adicionar o comportamento esperado. E a cada novo comportamento, você cria um novo elemento que o representará. Só que tem um detalhe, o Visitor vai agir em uma estrutura de objetos e cada objeto pode ter um comportamento distinto.  Por isso esse padrão sugere que você tenha vários métodos para manipular elementos distintos (visitElementA, VisitElementB, etc…)

Como o Visitor sabe qual método será apropriado para cada elemento?

Tem uma técnica chamada Double Dispatch (despacho duplo) que assegura a escolha correta de um método a ser invocado com base no tipo de receptor e argumento. Ele é parecido com o Padrão Strategy, porém não se engane. Eles são distintos!

Olhando para o Diagrama de Classe, observe que os elementos (Element) que receberão as novas operações, precisarão aceitar (accept) o visitante. Ou seja,  O método accept do Element recebe um objeto visitante e chama o método do elemento apropriado.

Como o objeto visitante possui vários métodos visitElement*, com base no tipo de elemento, aqui temos duas chamadas (Double Dispatch) que especificam o elemento e a operação correta para o elemento (com base em seu tipo).

Exemplo de uso do padrão Visitor

Pelo fato dele  representar uma operação a ser executada nos elementos de uma estrutura de objetos, ele pode ser combinado com os Padrões de Projetos Iterator  e Composite. Para deixar bastante didático, no artigo que fiz sobre o padrão Composite, fiz um exemplo criando uma estrutura de objetos para compor uma árvore de tags HTML. Suponha que queiramos adicionar operações a estes objetos, como faríamos com o Visitor?

Para não ficar repetitivo, vou remodelar o exemplo do Composite e vamos unir com os elementos do padrão Visitor. Veja no diagrama a seguir como ficará parte do nosso exemplo:

Diagrama Composite adaptado
Diagrama Composite adaptado
<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Visitor;

interface Element
{
    public function accept(Visitor $visitor): void;
}
<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Visitor;

abstract class HtmlTag implements Element
{    
    protected string $startTag; 
    protected string $endTag;

    public function setStartTag(string $tag): void
    {
        $this->startTag = $tag;        
    }

    public function getStartTag(): string
    {
        return $this->startTag;
    }

    public function setEndTag(string $tag): void
    {
        $this->endTag = $tag;
    }

    public function addChildTag(HtmlTag $htmlTag): void {
        throw new \BadMethodCallException("Current operation is not support for this object");
    }

    public function getChildren(){
        throw new \BadMethodCallException("Current operation is not support for this object");
    }

    public abstract function generateHtml(): void;
}
<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Visitor;

class HtmlElement extends HtmlTag
{
    private ?string $content;

    public function __construct(?string $content = '') 
    {
        $this->content = $content;
    }

    public function generateHtml(): void
    {
        if ($this->content) {
            echo "{$this->startTag} {$this->content} {$this->endTag}\n";
        } else {
            echo "{$this->startTag}\n";
        }
    }

    public function accept(Visitor $visitor): void
    {
        $visitor->visitHtmlElement($this);
    }
}
<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Visitor;

use SplObjectStorage;

class HtmlParentElement extends HtmlTag
{
    private SplObjectStorage $childrenTag;

    public function __construct()
    {
        $this->childrenTag = new SplObjectStorage();
    }

    public function addChildTag(HtmlTag $htmlTag): void
    {
        $this->childrenTag->attach($htmlTag);
    }

    public function removeChildTag(HtmlTag $htmlTag): void
    {
        $this->childrenTag->detach($htmlTag);
    }

    public function getChild(HtmlTag $htmlTag): HtmlTag
    {
        if (!$this->childrenTag->contains($htmlTag)) {
            throw new \InvalidArgumentException('HtmlTag not found');
        }
        return $htmlTag;
    }

    public function generateHtml(): void
    {
        printf("%s\n",$this->startTag);
        foreach($this->childrenTag as $tag){
            $tag->generateHtml();
        }
        printf("%s\n",$this->endTag);
    }

    public function accept(Visitor $visitor): void
    {
        $visitor->visitHtmlParentElement($this);
    }
}

Observe que já estamos utilizando recursos do Visitor através do método accept.

Agora vamos pensar, qual operação devemos definir no Visitor que gere alguma ação nos elementos? Este artigo está cheio de suposições, então, suponha mais uma vez que queiramos tratar as propriedades das tags de forma distintas com uma propriedade correspondente ao seu nível hierárquico. Para as tags child, vamos setar uma propriedade class com o valor visitor-child e nos elementos compostos com o valor visitor-parent.

Para reforçar este conceito, além desta ação de adicionar uma propriedade class, vamos criar mais um Visitor Concreto que será responsável por definir a tag style com um valor distinto para cada nível (child e parent), para esse exemplo vamos trocar o valor do width da tag style.

Diagrama e Classe Visitor Adaptado
Diagrama e Classe Visitor Adaptado
<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Visitor;

interface Visitor
{
    public function visitHtmlElement(HtmlElement $element): void;

    public function visitHtmlParentElement(HtmlParentElement $parentElement): void;
}
<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Visitor;

final class ClassPropertyVisitor implements Visitor
{
    private function changeProperty(HtmlTag $tag, string $className): string
    {
        $tagName = str_replace(['<','>'], '', $tag->getStartTag());
        return sprintf("<%s class=\"%s\">", $tagName, $className);
    }
    
    public function visitHtmlElement(HtmlElement $element): void
    {
        $element->setStartTag($this->changeProperty($element, 'visitor-child'));
    }

    public function visitHtmlParentElement(HtmlParentElement $parentElement): void
    {
        $parentElement->setStartTag($this->changeProperty($parentElement, 'visitor-parent'));
    }
}
<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Visitor;

final class StylePropertyVisitor implements Visitor
{
    private function changeProperty(HtmlTag $tag, int $width): string
    {
        $tagName = str_replace(['<','>'], '', $tag->getStartTag());
        return sprintf("<%s style=\"width:%dpx;\">", $tagName, $width);
    }
    
    public function visitHtmlElement(HtmlElement $element): void
    {
        $element->setStartTag($this->changeProperty($element, 580));
    }

    public function visitHtmlParentElement(HtmlParentElement $parentElement): void
    {
        $parentElement->setStartTag($this->changeProperty($parentElement, 758));
    }
}

Observe que o Visitor é independente, porém consegue adicionar ações a estrutura de dados que fizemos através do uso do Composite. Como este exemplo tem muitas classes, veja o diagrama completo para contextualizar a relação entre os objetos.

Diagrama completo do Visitor Pattern
Diagrama completo do Visitor Pattern

Parece complexo, mas veja na implementação dos testes, vai ficar mais claro a relação completa entre os elementos e como o Padrão de Projeto Visitor é implementado na prática. Veja:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Tests\Behavioral\Visitor;


use Growthdev\DesignPatterns\Behavioral\Visitor\HtmlElement;
use Growthdev\DesignPatterns\Behavioral\Visitor\HtmlParentElement;
use Growthdev\DesignPatterns\Behavioral\Visitor\ClassPropertyVisitor;
use Growthdev\DesignPatterns\Behavioral\Visitor\StylePropertyVisitor;
use PHPUnit\Framework\TestCase;

final class VisitorTest extends TestCase
{
    public function testVisitor(): void
    {
        $class = new ClassPropertyVisitor();
        $style = new StylePropertyVisitor();
         
        $html = new HtmlParentElement();
        $html->setStartTag("<html>");
        

        $body = new HtmlParentElement();
        $body->setStartTag("<body>");
        $body->accept($style);
        $body->accept($class);    
            
        $html->addChildTag($body);   
        
        $p = new HtmlElement("Testing html tag library");
        $p->setStartTag("<p>");
        $p->accept($style);
        $p->accept($class);

        $p->setEndTag("</p>");
        
        $html->getChild($body)->addChildTag($p);
        $html->getChild($body)->setEndTag("</body>");

        $html->setEndTag("</html>");
        $html->generateHtml();

        $this->expectOutputString(
            "<html>\n"
            ."<body style=\"width:758px;\" class=\"visitor-parent\">\n"
            ."<p style=\"width:580px;\" class=\"visitor-child\"> Testing html tag library </p>\n"
            ."</body>\n"
            ."</html>\n"
        );
    }
}

Uma grande desvantagem do padrão Visitor é que é difícil aumentar os tipos de elementos. Ou seja, se o tipo de elemento for alterado, o código-fonte da interface Visitor precisa ser modificado, e isso viola o Open Closed Principle do SOLID. Mas como tudo na vida tem vantagens e desvantagens, sempre é importante prestar atenção a estas questões arquiteturais antes de fazermos qualquer implementação.

Este artigo pertence a uma série onde explico todos os padrões de Projetos do Livro GOF em PHP, com os recursos arquiteturais mais avançados e com exemplos práticos. Fiz um resumo dos padrões de projetos para te ajudar a organizar este catálogo. Além disso, o código-fonte de todos os exemplos estão disponíveis no meu Github:

https://github.com/growthdev-repo/design-patterns

Se você já utiliza as técnicas de uso de Padrões de Projetos, deixa aqui um comentário com algum exemplo que possa ajudar outros DEVs. E se gostou do artigo, não esqueça de compartilhar. Isso ajuda muito!!!

Até o próximo artigo!!

Confiança Sempre!!!

Publicado emDesign PatternPadrões de ProjetosPHP

Seja o primeiro a comentar

    Deixe um comentário

    O seu endereço de e-mail não será publicado.