6.10. Visitor

  • EN: Visitor

  • PL: Odwiedzający

  • Type: object

The Visitor design pattern is a behavioral design pattern that allows you to add new behaviors to existing class hierarchies without altering any existing code. It works by creating a separate visitor class that implements all the appropriate specializations of a particular operation for all classes of an object structure.

Here's a simple example of the Visitor pattern in Python:

>>> class Element:
...     def accept(self, visitor):
...         visitor.visit(self)
...
>>> class ConcreteElementA(Element):
...     def accept(self, visitor):
...         visitor.visit_concrete_element_a(self)
...
>>> class ConcreteElementB(Element):
...     def accept(self, visitor):
...         visitor.visit_concrete_element_b(self)
...
>>> class Visitor:
...     def visit(self, element):
...         pass
...
>>> class ConcreteVisitor1(Visitor):
...     def visit_concrete_element_a(self, element):
...         print("ConcreteVisitor1 visited ConcreteElementA")
...
...     def visit_concrete_element_b(self, element):
...         print("ConcreteVisitor1 visited ConcreteElementB")
...
>>> class ConcreteVisitor2(Visitor):
...     def visit_concrete_element_a(self, element):
...         print("ConcreteVisitor2 visited ConcreteElementA")
...
...     def visit_concrete_element_b(self, element):
...         print("ConcreteVisitor2 visited ConcreteElementB")
...
>>> element_a = ConcreteElementA()
>>> element_b = ConcreteElementB()
>>> visitor1 = ConcreteVisitor1()
>>> visitor2 = ConcreteVisitor2()
>>> element_a.accept(visitor1)
ConcreteVisitor1 visited ConcreteElementA
>>> element_b.accept(visitor1)
ConcreteVisitor1 visited ConcreteElementB
>>> element_a.accept(visitor2)
ConcreteVisitor2 visited ConcreteElementA
>>> element_b.accept(visitor2)
ConcreteVisitor2 visited ConcreteElementB

In this example, Element is an interface for all types of visitable classes. ConcreteElementA and ConcreteElementB are concrete classes that implement the Element interface and define the accept method. The Visitor class has a method visit that takes an Element object as an argument. The ConcreteVisitor1 and ConcreteVisitor2 classes implement the Visitor interface and define their own versions of the visit method.

6.10.1. Pattern

  • Add new operations to an object structure without modifying it

  • For building editors

  • Open/Close Principle

../../_images/designpatterns-visitor-pattern.png

6.10.2. Problem

design-patterns/behavioral/img/designpatterns-visitor-problem.png


6.10.3. Solution

../../_images/designpatterns-visitor-solution.png

from abc import ABC, abstractmethod
from dataclasses import dataclass, field


class HtmlNode(ABC):
    @abstractmethod
    def execute(self, operation: 'Operation') -> None:
        pass

class HeadingNode(HtmlNode):
    def execute(self, operation: 'Operation') -> None:
        operation.apply_heading(self)

class AnchorNode(HtmlNode):
    def execute(self, operation: 'Operation') -> None:
        operation.apply_anchor(self)


class Operation(ABC):
    """Visitor"""

    @abstractmethod
    def apply_heading(self, heading: HeadingNode) -> None:
        pass

    @abstractmethod
    def apply_anchor(self, anchor: AnchorNode) -> None:
        pass

class HighlightOperation(Operation):
    def apply_heading(self, heading: HeadingNode) -> None:
        print('highlight-heading')

    def apply_anchor(self, anchor: AnchorNode) -> None:
        print('apply-anchor')

class PlaintextOperation(Operation):
    def apply_heading(self, heading: HeadingNode) -> None:
        print('text-heading')

    def apply_anchor(self, anchor: AnchorNode) -> None:
        print('text-anchor')


@dataclass
class HtmlDocument:
    nodes: list[HtmlNode] = field(default_factory=list)

    def add(self, node: HtmlNode) -> None:
        self.nodes.append(node)

    def execute(self, operation: Operation) -> None:
        for node in self.nodes:
            node.execute(operation)


if __name__ == '__main__':
    document = HtmlDocument()
    document.add(HeadingNode())
    document.add(AnchorNode())
    document.execute(PlaintextOperation())


# class Operation:
#     @abstractmethod
#     @singledispatchmethod
#     def apply(arg):
#         raise NotImplementedError('Argument must be HtmlNode')
#
#     @abstractmethod
#     @apply.register
#     def _(self, heading: HeadingNode):
#         pass
#
#     @abstractmethod
#     @apply.register
#     def _(self, anchor: AnchorNode):
#         pass

6.10.4. Assignments